Hack The Boo
Hack The Boo 2023 was a CTF event held by hackthebox, I never really participated in any public CTF events but always wanted to try it.
In this post I’ll explain how I solved a few interesting challenges.
Spellbrewery
This challenge was a reverse engineering one. It gave us a zip file with a Dotnet app and a DLL file. When executing the app you’d get a prompt like this:
1. List Ingredients
2. Display Current Recipe
3. Add Ingredient
4. Brew Spell
5. Clear Recipe
6. Quit
The objective is to brew a spell with the correct ingredients, that should give us the flag. By brewing the spell with the wrong ingredients we get a error message and nothing else.
After some googling I found out that ILSpy might be a good tool to decompile Dotnet apps. After setting it up I managed to decompile the DLL. Inside it I found the implementation of the brewSpell
method:
private static void BrewSpell()
{
if (recipe.get_Count() < 1)
{
Console.WriteLine("You can't brew with an empty cauldron");
return;
}
byte[] array = Enumerable.ToArray<byte>(Enumerable.Select<Ingredient, byte>((System.Collections.Generic.IEnumerable<Ingredient>)recipe, (Func<Ingredient, byte>)((Ingredient ing) => (byte)(System.Array.IndexOf<string>(IngredientNames, ((object)ing).ToString()) + 32))));
if (Enumerable.SequenceEqual<Ingredient>((System.Collections.Generic.IEnumerable<Ingredient>)recipe, Enumerable.Select<string, Ingredient>((System.Collections.Generic.IEnumerable<string>)correct, (Func<string, Ingredient>)((string name) => new Ingredient(name)))))
{
Console.WriteLine("The spell is complete - your flag is: " + Encoding.get_ASCII().GetString(array));
Environment.Exit(0);
}
else
{
Console.WriteLine("The cauldron bubbles as your ingredients melt away. Try another recipe.");
}
}
The correct
variable there looks suspicious, turns out it’s a constant that we can inspect in the decompiled DLL:
private static readonly string[] correct = new string[36]
{
"Phantom Firefly Wing", "Ghastly Gourd", "Hocus Pocus Powder", "Spider Sling Silk", "Goblin's Gold", "Wraith's Tear", "Werewolf Whisker", "Ghoulish Goblet", "Cursed Skull", "Dragon's Scale Shimmer",
"Raven Feather", "Dragon's Scale Shimmer", "Zombie Zest Zest", "Ghoulish Goblet", "Werewolf Whisker", "Cursed Skull", "Dragon's Scale Shimmer", "Haunted Hay Bale", "Wraith's Tear", "Zombie Zest Zest",
"Serpent Scale", "Wraith's Tear", "Cursed Crypt Key", "Dragon's Scale Shimmer", "Salamander's Tail", "Raven Feather", "Wolfsbane", "Frankenstein's Lab Liquid", "Zombie Zest Zest", "Cursed Skull",
"Ghoulish Goblet", "Dragon's Scale Shimmer", "Cursed Crypt Key", "Wraith's Tear", "Black Cat's Meow", "Wraith Whisper"
};
After inputing that as the answer I got the flag!
Spookycheck
This one was also a reverse engineering challenge. It gave us a check.pyc
file. In the Python world .pyc files are basically files with the compiled python bytecode, we can execute them as if they were a regular python script.
By running it we’d get the following prompt:
🎃 Welcome to SpookyCheck 🎃
🎃 Enter your password for spooky evaluation 🎃
After some research I found out that there’s a module included with Python that can be used for disassembly. I wrote the following short script to spit out the disassembled bytecode:
import dis
import marshal
with open('check.pyc', 'rb') as f:
f.seek(16)
dis.dis(marshal.load(f))
Here’s the full disassembled code:
0 0 RESUME 0
1 2 LOAD_CONST 0 (b'SUP3RS3CR3TK3Y')
4 STORE_NAME 0 (KEY)
2 6 PUSH_NULL
8 LOAD_NAME 1 (bytearray)
10 LOAD_CONST 1 (b'\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\')
12 PRECALL 1
16 CALL 1
26 STORE_NAME 2 (CHECK)
4 28 LOAD_CONST 2 (<code object transform at 0x1004ea1f0, file "check.py", line 4>)
30 MAKE_FUNCTION 0
32 STORE_NAME 3 (transform)
10 34 LOAD_CONST 3 (<code object check at 0x1004eaa60, file "check.py", line 10>)
36 MAKE_FUNCTION 0
38 STORE_NAME 4 (check)
13 40 LOAD_NAME 5 (__name__)
42 LOAD_CONST 4 ('__main__')
44 COMPARE_OP 2 (==)
50 POP_JUMP_FORWARD_IF_FALSE 88 (to 228)
14 52 PUSH_NULL
54 LOAD_NAME 6 (print)
56 LOAD_CONST 5 ('🎃 Welcome to SpookyCheck 🎃')
58 PRECALL 1
62 CALL 1
72 POP_TOP
15 74 PUSH_NULL
76 LOAD_NAME 6 (print)
78 LOAD_CONST 6 ('🎃 Enter your password for spooky evaluation 🎃')
80 PRECALL 1
84 CALL 1
94 POP_TOP
16 96 PUSH_NULL
98 LOAD_NAME 7 (input)
100 LOAD_CONST 7 ('👻 ')
102 PRECALL 1
106 CALL 1
116 STORE_NAME 8 (inp)
17 118 PUSH_NULL
120 LOAD_NAME 4 (check)
122 LOAD_NAME 8 (inp)
124 LOAD_METHOD 9 (encode)
146 PRECALL 0
150 CALL 0
160 PRECALL 1
164 CALL 1
174 POP_JUMP_FORWARD_IF_FALSE 13 (to 202)
18 176 PUSH_NULL
178 LOAD_NAME 6 (print)
180 LOAD_CONST 8 ("🦇 Well done, you're spookier than most! 🦇")
182 PRECALL 1
186 CALL 1
196 POP_TOP
198 LOAD_CONST 10 (None)
200 RETURN_VALUE
20 >> 202 PUSH_NULL
204 LOAD_NAME 6 (print)
206 LOAD_CONST 9 ('💀 Not spooky enough, please try again later 💀')
208 PRECALL 1
212 CALL 1
222 POP_TOP
224 LOAD_CONST 10 (None)
226 RETURN_VALUE
13 >> 228 LOAD_CONST 10 (None)
230 RETURN_VALUE
Disassembly of <code object transform at 0x1004ea1f0, file "check.py", line 4>:
4 0 RESUME 0
5 2 LOAD_CONST 1 (<code object <listcomp> at 0x100451f10, file "check.py", line 5>)
4 MAKE_FUNCTION 0
7 6 LOAD_GLOBAL 1 (NULL + enumerate)
18 LOAD_FAST 0 (flag)
20 PRECALL 1
24 CALL 1
5 34 GET_ITER
36 PRECALL 0
40 CALL 0
50 RETURN_VALUE
Disassembly of <code object <listcomp> at 0x100451f10, file "check.py", line 5>:
5 0 RESUME 0
2 BUILD_LIST 0
4 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 54 (to 116)
7 8 UNPACK_SEQUENCE 2
12 STORE_FAST 1 (i)
14 STORE_FAST 2 (f)
6 16 LOAD_FAST 2 (f)
18 LOAD_CONST 0 (24)
20 BINARY_OP 0 (+)
24 LOAD_CONST 1 (255)
26 BINARY_OP 1 (&)
30 LOAD_GLOBAL 0 (KEY)
42 LOAD_FAST 1 (i)
44 LOAD_GLOBAL 3 (NULL + len)
56 LOAD_GLOBAL 0 (KEY)
68 PRECALL 1
72 CALL 1
82 BINARY_OP 6 (%)
86 BINARY_SUBSCR
96 BINARY_OP 12 (^)
100 LOAD_CONST 2 (74)
102 BINARY_OP 10 (-)
106 LOAD_CONST 1 (255)
108 BINARY_OP 1 (&)
5 112 LIST_APPEND 2
114 JUMP_BACKWARD 55 (to 6)
>> 116 RETURN_VALUE
Disassembly of <code object check at 0x1004eaa60, file "check.py", line 10>:
10 0 RESUME 0
11 2 LOAD_GLOBAL 1 (NULL + transform)
14 LOAD_FAST 0 (flag)
16 PRECALL 1
20 CALL 1
30 LOAD_GLOBAL 2 (CHECK)
42 COMPARE_OP 2 (==)
48 RETURN_VALUE
A few pieces stand out, first we have those two constants called KEY
and CHECK
.
There’s also a function called transform
, with a parameter called flag that uses a list comprehension inside it, the list comprehension, seems like it’s iterating over the flag variable and transforming it by doing a bunch of operations with some using the KEY
constant. After all that there’s a check
function that calls transform over the flag
variable and then checks it against the CHECK
constant.
That seems to indicate that the CHECK
constant has the encrypted flag, and it is encrypted using the KEY
constant. So to get the flag we probably have to reverse the process. Here I decided to cheat a bit, I never laid eyes on Python bytecode before, but it all seemed rather structured and relatively higher leveled, so I wondered, if I give ChatGPT the bytecode for the transform function and the list comprehension, could it write me a script to reverse the process? Turns out it could, and here’s what it wrote:
KEY = b'SUP3RS3CR3TK3Y'
CHECK = b'\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\'
def reverse_transform(byte_val, index):
# Add 74
byte_val += 74
# XOR with the byte from KEY
byte_val ^= KEY[(index + len(KEY)) % len(KEY)]
# Subtract 24
byte_val -= 24
# Ensure it remains a byte value
byte_val &= 255
return byte_val
original_input = bytearray()
for i, byte_val in enumerate(CHECK):
original_input.append(reverse_transform(byte_val, i))
print(original_input.decode())
It worked on the first try and I got the flag! Thanks ChatGPT!
Ghostly Templates
This was a web challenge. They gave us a Golang app that looks like this:
package main
import (
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
const WEB_PORT = "1337"
const TEMPLATE_DIR = "./templates"
type LocationInfo struct {
Status string `json:"status"`
Country string `json:"country"`
CountryCode string `json:"countryCode"`
Region string `json:"region"`
RegionName string `json:"regionName"`
City string `json:"city"`
Zip string `json:"zip"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Timezone string `json:"timezone"`
ISP string `json:"isp"`
Org string `json:"org"`
AS string `json:"as"`
Query string `json:"query"`
}
type MachineInfo struct {
Hostname string
OS string
KernelVersion string
Memory string
}
type RequestData struct {
ClientIP string
ClientUA string
ServerInfo MachineInfo
ClientIpInfo LocationInfo `json:"location"`
}
func GetServerInfo(command string) string {
out, err := exec.Command("sh", "-c", command).Output()
if err != nil {
return ""
}
return string(out)
}
func (p RequestData) GetLocationInfo(endpointURL string) (*LocationInfo, error) {
resp, err := http.Get(endpointURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP request failed with status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var locationInfo LocationInfo
if err := json.Unmarshal(body, &locationInfo); err != nil {
return nil, err
}
return &locationInfo, nil
}
func (p RequestData) IsSubdirectory(basePath, path string) bool {
rel, err := filepath.Rel(basePath, path)
if err != nil {
return false
}
return !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func (p RequestData) OutFileContents(filePath string) string {
data, err := os.ReadFile(filePath)
if err != nil {
return err.Error()
}
return string(data)
}
func readRemoteFile(url string) (string, error) {
response, err := http.Get(url)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP request failed with status code: %d", response.StatusCode)
}
content, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
return string(content), nil
}
func getIndex(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/view?page=index.tpl", http.StatusMovedPermanently)
}
func getTpl(w http.ResponseWriter, r *http.Request) {
var page string = r.URL.Query().Get("page")
var remote string = r.URL.Query().Get("remote")
if page == "" {
http.Error(w, "Missing required parameters", http.StatusBadRequest)
return
}
reqData := &RequestData{}
userIPCookie, err := r.Cookie("user_ip")
clientIP := ""
if err == nil {
clientIP = userIPCookie.Value
} else {
clientIP = strings.Split(r.RemoteAddr, ":")[0]
}
userAgent := r.Header.Get("User-Agent")
locationInfo, err := reqData.GetLocationInfo("https://freeipapi.com/api/json/" + clientIP)
if err != nil {
http.Error(w, "Could not fetch IP location info", http.StatusInternalServerError)
return
}
reqData.ClientIP = clientIP
reqData.ClientUA = userAgent
reqData.ClientIpInfo = *locationInfo
reqData.ServerInfo.Hostname = GetServerInfo("hostname")
reqData.ServerInfo.OS = GetServerInfo("cat /etc/os-release | grep PRETTY_NAME | cut -d '\"' -f 2")
reqData.ServerInfo.KernelVersion = GetServerInfo("uname -r")
reqData.ServerInfo.Memory = GetServerInfo("free -h | awk '/^Mem/{print $2}'")
var tmplFile string
if remote == "true" {
tmplFile, err = readRemoteFile(page)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
} else {
if !reqData.IsSubdirectory("./", TEMPLATE_DIR+"/"+page) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
tmplFile = reqData.OutFileContents(TEMPLATE_DIR + "/" + page)
}
tmpl, err := template.New("page").Parse(tmplFile)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, reqData)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", getIndex)
mux.HandleFunc("/view", getTpl)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
fmt.Println("Server started at port " + WEB_PORT)
http.ListenAndServe(":"+WEB_PORT, mux)
}
Just like the other web challenge the flag was in a file inside the root of the container.
Basically the app exposes three things:
- The index route which just redirects to the view route.
- The view route which populates the RequestData struct and renders a template using it as data.
- The static route which is used to serve static content to the app.
The interesting route here is the view one, it has a bunch of interesting stuff, first of all the template is user supplied data, you can either specify a local template that’s already in the app, or give it a url so it can download a template to render.
The first thing that called my attention was the IsSubdirectory
function, my first thought was that if I could bypass the traversal protections there we could simply get the app the render the flag.txt file as a template and we’d see the flag. I had no luck bypassing it though so I decided to change my strategy.
After reading a bit about golang html/templates I learned that if you send a struct to the template context inside the template you get access to all the functions for that struct. If you look at the RequestData
functions you’ll see that OutFileContents
is one of them, this function will accept a single parameter containing a file path and will return the contents of the file, exactly what we need, how convenient!
To exploit it we simply need to host somewhere a template that looks like this:
<html>
<div>
{{ .OutFileContents "/flag.txt" }}
</div>
</html>
I used pastebin to upload the template and using it’s url I got the flag.