Backstory

Mid-2020 I was frequently on the go and since work is remote it didn’t matter where I was situated. Most hotels had availability however, one in particular that I enjoyed staying at from time to time was always booked weeks out. Of course, I would call them to double-check and inevitably receive the “yep, we are fully booked”. So the story goes, I went to visit their booking website a few weeks before my requested date and of course they were fully booked. Out of curiosity, I started to poke around and noticed when a user tries to book a room, their frontend sends an XHR POST request to a backend API server for date availability validation. There! I now have a silver lining and a possibility to get a room! Here, the idea was born.

My Solution

After discovering the backend API endpoint, I further poked around and realized it could be utilized for automating checks on date availability. I now have a method for knowing the exact moment a room becomes available and ultimately be the first to reserve it. I took my findings and wrote some code to automate the complete process. My goal for the code was to hit the the backend API for specified dates, wrap it into a job that runs every ~5 minutes. If the stars align and the backend API returns with availability for my requested dates, I programmed a hook to execute and email me with a link embedded in the message body as the exact booking URL that I can quickly click to reserve the room.

     

Why Is This Possible?

During my investigation, I established some reasoning my proposed solution should work:

  • The backend API that serves as a ledger for reservations is public. Usually these are firewalled and only respond to trusted resources.
  • The backend API implements poor CORS access control. SOP is relaxed via a simple pre-flight XHR request, but there’s no Origin validation because Access-Control-Allow-Origin trusts any domain.
  • No authentication or anti-forgery token required by the backend API. I noticed other endpoints required a JWT or CSRF token for authentication/validation, but not the API. Access-Control-Allow-Headers Allows Authorization, but not enforced for POST requests.
  • No rate-limiting on the backend API. I didn’t observe any rate-limiting or whitelisting, which could prevent me from making any or the amount of requests I performed.

My Process

My tool set decision was solely based on speedy simplicity, so I exclusively utilized my command-line for everything performed. I used mitmproxy as a lightweight proxy to intercept, inspect and tamper with the traffic. I could have used tools like Burpsuite, but they are way too bulky for what I’m doing. Typically, I would use curl however, I need access to the DOM and execution of JavaScript for any performed XHR. Modern browsers perform all of this automatically, however on the command-line I need a tool to execute these actions, so I’m utilizing phantomjs. Finally, I code everything in Python to request, parse and shoot myself an email when my specified dates are available.

     

Tools

     

Exploring the Backend API

First, I start mitmproxy and perform a request with phantomjs to the main Hotel website.

mitmproxy -p 5555
phantomjs --ignore-ssl-errors=true --proxy=http://127.0.0.1:5555 netlog.js "https://book-this-awesome-website.com/1094?DateOut=7/19/2020&DateIn=7/15/2020"

     

I then investigate the traffic in mitmproxy and notice a POST request directed to a backend API with dates I specified! Directly before the POST a pre-flight OPTIONS request was sent per CORS. Since the Content-Type is application/json the CORS rules validate if it’s safe to send the subsequent POST.

     

Phantom and cURL Req r

     

While inspecting the POST request flow, I take note of how the data payload is structured and sent to the backend API.

{"queryString":{"dateout":"7/19/2020","datein":"7/15/2020","devicetype":"Web4_Desktop"},"cookies":[]}

     

Inspecting the POST response flow, I noticed SOP is relaxed via the OPTIONS XHR request, but there’s no Origin validation since the Access-Control-Allow-Origin returns trust for any domain. I also noticed my POST request didn’t send or require a JWT or anti-forgery token. I do, however, notice a key “ErrorCount” with the value “3” in the JSON response body. To me, this looks like logic that tells clients “Your dates are not available”.

     

Phantom and cURL Req r

     

Next, I duplicate the POST flow and perform some tests by modifying the POST request data payload and take note of the response. I replay the POST for dates I’ve confirmed are available.

{"queryString":{"dateout":"10/22/2020","datein":"10/21/2020","devicetype":"Web4_Desktop"},"cookies":[]}

     

I receieved a response with key “ErrorCount” with the value “0” in the JSON response body.

     

Phantom and cURL Req r

     

Based on the first response, this is where I decided to define my truthy and falsy logic for date availability by performing basic Boolean operations. I then Export to a file as a curl request and start coding up the job!

     

Codify

At this point, I can start writing the code. mitmproxy has an option to export a given flow in curl format. The devs removed export to python requests however, there’s an easy way to convert curl to requests with uncurl

     

In mitmproxy export as curl

export.file curl @focus hotel_hijinks.txt

     

Convert to requests

cat hotel_hijinks.txt | uncurl > hotel_hijinks.py 

     

Now putting it all together with some code

import sys
import requests
import yagmail

from smtplib import SMTPAuthenticationError

DATEIN = "07/15/2020"
DATEOUT = "07/19/2020"
PASS = "MYPASS"
USER = "my-sender-email@example.com"
TO = "my-receiver-email@example.com"
SUBJECT = "Hotel Date Available!"
MESSAGE = """Go to https://book-this-awesome-website.com/1094?DateIn={0}&DateOut={1}#/datesofstay
          to book your reservation!
          """.format(DATEIN, DATEOUT)

response = requests.post(
    "https://api.travelclick.com/ibe-shop/v1/hotel/1094/avail-booking-mask",
    data='{{"queryString":{{"dateout":"{1}","datein":"{0}","devicetype":"Web4_Desktop"}},"cookies":[]}}'.format(
        DATEIN, DATEOUT
    ),
    headers={
        "User-Agent": "Mozilla/5.0",
        "Accept-Language": "en-US,*",
        "x-tc-header": "USD",
        "Content-Type": "application/json;charset=UTF-8",
        "Origin": "doesntmatter",
        "Connection": "Keep-Alive",
    },
    cookies={},
    auth=(),
)

if response.json()["errorList"]["errors"]:
    sys.exit(1)
else:
    try:
        with yagmail.SMTP(USER, PASS) as yag:
            yag.send(TO, SUBJECT, MESSAGE)
            print("Sent email successfully.")
    except SMTPAuthenticationError:
        exit(1)
    sys.exit(0)

     

Finally, to make the job as simple as possible, I chose to use the Bash reserved keyword until. The until loop will execute a given command as long as the condition evaluates to False or any return code than 0. The loop is terminated as soon as the first expression evaluates to True or return code 0.

until python hotel_hijinks.py; do sleep 300; done

                             

After setting the job up on my server to run for the next several weeks, I just went on about my business. I knew there was no guarantee the dates I wanted would become available, but I did know that if they did I wanted to be first to know. Then one random day I received an email! It worked! My dates became available! For sure I couldn’t believe that my frustration and curiosity gifted me an available room at my favorite hotel.