idekCTF에서 Web 문제를 풀지 못했습니다. 그래서 리뷰하려고 다른 분들의 writeup을 참고하면서 대회 끝나고 풀었습니다! 공부를 위해 정리하는 글이기 때문에 자세한 내용은 제가 참고한 github를 방문해 주세요!
https://github.com/daeMOn63/ctf-writeups/tree/main/idek22/readme
idekCTF2022 Web / Readme writeup
문제는 다음과 같습니다. flag를 읽으면 되는 문제이다. 문제는 golang으로 되어 있어서 go 언어를 처음 접하는 저에게는 코드 읽기부터 쉽지 않았다. 이번 기회를 통해 go 언어를 조금 깨달은 것 같다.
문제 사이트에 접속하면 다음과 같이 오류가 나온다. 코드를 보고 시작해 보자.
코드분석
package main
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os"
"time"
)
var password = sha256.Sum256([]byte("idek"))
var randomData []byte
const (
MaxOrders = 10
)
func initRandomData() {
rand.Seed(1337)
randomData = make([]byte, 24576)
if _, err := rand.Read(randomData); err != nil {
panic(err)
}
copy(randomData[12625:], password[:])
}
type ReadOrderReq struct {
Orders []int `json:"orders"`
}
func justReadIt(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("bad request\n"))
return
}
reqData := ReadOrderReq{}
if err := json.Unmarshal(body, &reqData); err != nil {
w.WriteHeader(500)
w.Write([]byte("invalid body\n"))
return
}
if len(reqData.Orders) > MaxOrders {
w.WriteHeader(500)
w.Write([]byte("whoa there, max 10 orders!\n"))
return
}
reader := bytes.NewReader(randomData)
validator := NewValidator()
ctx := context.Background()
for _, o := range reqData.Orders {
if err := validator.CheckReadOrder(o); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return
}
ctx = WithValidatorCtx(ctx, reader, int(o))
_, err := validator.Read(ctx)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("failed to read: %v\n", err)))
return
}
}
if err := validator.Validate(ctx); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("validation failed: %v\n", err)))
return
}
w.WriteHeader(200)
w.Write([]byte(os.Getenv("FLAG")))
}
func main() {
if _, exists := os.LookupEnv("LISTEN_ADDR"); !exists {
panic("env LISTEN_ADDR is required")
}
if _, exists := os.LookupEnv("FLAG"); !exists {
panic("env FLAG is required")
}
initRandomData()
http.HandleFunc("/just-read-it", justReadIt)
srv := http.Server{
Addr: os.Getenv("LISTEN_ADDR"),
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
fmt.Printf("Server listening on %s\n", os.Getenv("LISTEN_ADDR"))
if err := srv.ListenAndServe(); err != nil {
panic(err)
}
}
type Validator struct{}
func NewValidator() *Validator {
return &Validator{}
}
func (v *Validator) CheckReadOrder(o int) error {
if o <= 0 || o > 100 {
return fmt.Errorf("invalid order %v", o)
}
return nil
}
func (v *Validator) Read(ctx context.Context) ([]byte, error) {
r, s := GetValidatorCtxData(ctx)
buf := make([]byte, s)
_, err := r.Read(buf)
if err != nil {
return nil, fmt.Errorf("read error: %v", err)
}
return buf, nil
}
func (v *Validator) Validate(ctx context.Context) error {
r, _ := GetValidatorCtxData(ctx)
buf, err := v.Read(WithValidatorCtx(ctx, r, 32))
if err != nil {
return err
}
if bytes.Compare(buf, password[:]) != 0 {
return errors.New("invalid password")
}
return nil
}
const (
reqValReaderKey = "readerKey"
reqValSizeKey = "reqValSize"
)
func GetValidatorCtxData(ctx context.Context) (io.Reader, int) {
reader := ctx.Value(reqValReaderKey).(io.Reader)
size := ctx.Value(reqValSizeKey).(int)
if size >= 100 {
reader = bufio.NewReader(reader)
}
return reader, size
}
func WithValidatorCtx(ctx context.Context, r io.Reader, size int) context.Context {
ctx = context.WithValue(ctx, reqValReaderKey, r)
ctx = context.WithValue(ctx, reqValSizeKey, size)
return ctx
}
전체 코드이다.
initRandomData()
http.HandleFunc("/just-read-it", justReadIt)
먼저 main함수를 보면 initRandomData 함수를 호출해서 버퍼를 초기화한다. 이후 http.HandleFunc함수를 통해 'just-read-it'이라는 페이지가 있다는 것을 확인했다.
invalid body가 출력되는 것을 보고 코드에서 찾아봤다.
func justReadIt(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("bad request\n"))
return
}
reqData := ReadOrderReq{}
if err := json.Unmarshal(body, &reqData); err != nil {
w.WriteHeader(500)
w.Write([]byte("invalid body\n"))
return
}
justReadIt 함수에서 출력하는 문장이다. JSON은 본문을 ReadOrderReq구조체로 Unmarshal한다. 이때 null이면 invaild body라고 출력되는 것을 확인했다.
if len(reqData.Orders) > MaxOrders {
w.WriteHeader(500)
w.Write([]byte("whoa there, max 10 orders!\n"))
return
}
이후 orders 구조체를 통해 1~10개의 데이터가 있는지 확인한다.
{"orders": [1,2,3...]}
따라서 위와 같이 json string을 구성해야 한다. 필자는 여기서 막혔다.
reader := bytes.NewReader(randomData)
validator := NewValidator()
이후 새로운 Reader와 Validator를 생성한다.
ctx := context.Background()
for _, o := range reqData.Orders {
if err := validator.CheckReadOrder(o); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return
}
ctx = WithValidatorCtx(ctx, reader, int(o))
_, err := validator.Read(ctx)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("failed to read: %v\n", err)))
return
}
}
if err := validator.Validate(ctx); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("validation failed: %v\n", err)))
return
}
w.WriteHeader(200)
w.Write([]byte(os.Getenv("FLAG")))
이후 ctx구조체를 생성한다.
func (v *Validator) CheckReadOrder(o int) error {
if o <= 0 || o > 100 {
return fmt.Errorf("invalid order %v", o)
}
return nil
}
지금부터가 중요한데 첫 if문에서는 CheckReadOrder 함수가 호출되면서 0 < o < =100 인지 검사한다.
ctx = WithValidatorCtx(ctx, reader, int(o))
func WithValidatorCtx(ctx context.Context, r io.Reader, size int) context.Context {
ctx = context.WithValue(ctx, reqValReaderKey, r)
ctx = context.WithValue(ctx, reqValSizeKey, size)
return ctx
}
문제가 없으면 context를 업데이트한다.
_, err := validator.Read(ctx)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("failed to read: %v\n", err)))
return
}
func (v *Validator) Read(ctx context.Context) ([]byte, error) {
r, s := GetValidatorCtxData(ctx)
buf := make([]byte, s)
_, err := r.Read(buf)
if err != nil {
return nil, fmt.Errorf("read error: %v", err)
}
return buf, nil
}
func GetValidatorCtxData(ctx context.Context) (io.Reader, int) {
reader := ctx.Value(reqValReaderKey).(io.Reader)
size := ctx.Value(reqValSizeKey).(int)
if size >= 100 {
reader = bufio.NewReader(reader)
}
return reader, size
}
Read를 통해 ctx구조체를 읽어온다. Read를 자세히 보면 GetValidatorCtxData함수를 호출하는데 이때 size >= 100이면 새로운 리더에 기존 리더가 래핑된다. CheckReadOrder에서 유효성 검사를 할 때도 보면 값이 100보다 클 수는 없지만 같을 수는 있다고 했다.
이 과정이 끝나면 r.Read(buf)로 버퍼 값을 읽어온다.
if err := validator.Validate(ctx); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("validation failed: %v\n", err)))
return
}
func (v *Validator) Validate(ctx context.Context) error {
r, _ := GetValidatorCtxData(ctx)
buf, err := v.Read(WithValidatorCtx(ctx, r, 32))
if err != nil {
return err
}
if bytes.Compare(buf, password[:]) != 0 {
return errors.New("invalid password")
}
return nil
}
이후 마지막으로 Validata를 호출해서 ctx를 32비트로 읽어온다. 읽은 내용을 buf 변수에 담고 password와 비교한다.
같으면 FLAG가 출력된다.
빨간색 글씨로 표시한 부분이 핵심이다. 100을 입력하면 기존 버퍼가 래핑 되면서 새로운 리더가 생성된다. 이때 새로운 리더는 defaultBufSize로 생성되기 때문에 4096의 크기로 생성이 된다.
func initRandomData() {
rand.Seed(1337)
randomData = make([]byte, 24576)
if _, err := rand.Read(randomData); err != nil {
panic(err)
}
copy(randomData[12625:], password[:])
}
따라서 100을 넣으면 4096의 사이즈로 들어간다. 그럼 이제 정확하게 12625 바이트를 넣어서 password를 읽을 수 있다.
12625/4096 = 3,082275391 // so 3 times 100
12625−(3*4096) = 337
337/99 = 3.404040 // so 3 times 99
337-(3*99) = 40 // so 1 time 40
그래서 100을 세 번 넣고, 99를 세 번 넣고, 나머지인 40을 넣으면 된다.
JSON Unmarshal
간단하게 말하면 json string -> 구조체 or map 변환하는 과정입니다.
http://golang.site/go/article/104-JSON-%EC%82%AC%EC%9A%A9
https://etloveguitar.tistory.com/44
진짜 라이트업 보면서 이해하는데 2시간은 썼다. 어렵지만 어려워서 재밌고, 보람차다.
'웹해킹 > CTF' 카테고리의 다른 글
knightCTF 2023 Web - GET Me Writeup (0) | 2023.01.23 |
---|---|
idekCTF2022 paywall writeup (0) | 2023.01.18 |
TUCTF 2022 Web My Assembly Line Writeup (1) | 2022.12.08 |
TUCTF 2022 Web Tornado Writeup (0) | 2022.12.06 |
화이트햇 콘테스트 2022 CTF Web Buffalo [Secret] (0) | 2022.11.23 |