Blog: How-Tos

How a Pentester books a Squash court

Alan Monie 31 Mar 2016

Introduction

I play Squash regularly at my local club, but there are only two courts, and being a commuter town, it’s a nightmare to get a court after 5pm. They have an on-line booking system and the courts are bookable 7 days in advance.

They become available at midnight for the following week, but by 8am, the courts are fully booked for the following week… unless you want to be playing Squash at 10pm.

Here’s a busy squash schedule:

squash1
Being a pentester, I often write simple scripts to automate parts of testing or to bulk collect results from multiple servers. My job isn’t always about finding vulnerabilities, but finding new and innovative ways of getting stuff done. So I decided I needed to hack together a script that would book a court for me so that I didn’t have to remember, a week before, at exactly midnight, to be sure of getting a court.

I first experimented with Bash, and then moved on to .Net.

The Bash solution

Version 1 of the Squash booking application was conceived. I wrote a simple bash script that would read in the day, time, and court code from a file, and attempt to book that court.

I apologise in advance for my code quality!

Bash script to read in the codes and book the court

#!/bin/bash
sleep 45
echo -n “Running script on: ” >> /root/squash/log.txt
date >> /root/squash/log.txt
grep -v `date +%d%m%y` /root/squash/times.txt >> /root/squash/new_times.txtgrep `date +%d%m%y` /root/squash/times.txt | while read -r COURT TIME DAY BOOKINGDATE; do
attempt=”0″
echo “New time” > /root/squash/http_response.html
while ! [[ `grep Reserved /root/squash/http_response.html` ]] && [[ $attempt -lt 40 ]]
do
attempt=$((attempt+1))
echo -n “Trying to book: ” >> /root/squash/log.txt
echo -n $COURT $TIME $DAY >> /root/squash/log.txt
echo -n ” on: ” >> /root/squash/log.txt
date >> /root/squash/log.txt
/root/squash/book_court_time_day.sh $COURT $TIME $DAY
sleep 1
done
if ! [[ `grep Reserved /root/squash/http_response.html` ]]
then
echo $COURT $TIME $DAY $BOOKINGDATE >> /root/squash/new_times.txt
echo “Booking failed.” >> /root/squash/log.txt
else
echo “Booked :-)” >> /root/squash/log.txt
fi
done
mv /root/squash/new_times.txt /root/squash/times.txt

Bash script to book a specific court at a specified day and time

#!/bin/bash
curl –data “selected_current_column=0&selected_current_court=$1&path=http%3A%2F%2Fwww.example.com%2Fcgi-bin%2Fscheduling%2Fsquashclub%2Fschedule.cgi&selection=$2&general_password=Open&mc=Open&selected_day=$3&[email protected]&password=mypassword&general_password=Open” http://www.example.com/cgi-bin/scheduling/squashclub/schedule.cgi -o /root/squash/http_response.html

Crontab to run the booking script at one minute to midnight

# m h dom mon dow command
59 23 * * * /root/squash/book_from_file.sh

The main body of code starts to run at 15 seconds before midnight. It greps through a times.txt file looking for courts to book on that day. If it finds one, it tries to book it every second for a maximum of 40 attempts. There is various logging output that is generated so that I can keep an eye on it. This served fine for months, but it was a bit clunky and I had to edit the times.txt and work out the day code. For example, the day code for the 30th March was 282. So if I wanted to book court 1 at 19:00, I’d have to enter “0 19 282 220316” into times.txt. This would be run on the 22/03/2016.

There were a number of problems with this approach. One was that it was awkward trying to calculate the day code and manually entering that into times.txt. Another was that I had to SSH into my Linux box to edit this file, so it wasn’t very mobile.
It was a bit of a pain, but at least I got a court! Version 2 was soon required…

A solution with .Net and the Google Calendar API

I use Google apps on my phone and organise my life using Google calendar. It would be great if I could just add an event into Google calendar and an app would book the court on the day and time that I wanted. I looked at trying to download my calendar daily and parse the results to generate a times.txt file, but I soon discovered that Google published an API (https://developers.google.com/google-apps/calendar/) to make all this easy. Great.

In order to use this API, you need to create a new project, enable the API in the Google API Manager (https://console.developers.google.com/apis), and create a service account. This is the account that your app will use to access the Google Calendar. The account uses a certificate for authentication, so there is no need to set up passwords on the account. Here’s the Google API manager:

squash2

You need to download a file containing the public/private key pair from the Google API Manager. You can then reference this from your .Net application to gain access to the API.

The last step is to give this service account permission to access your calendar. I changed the permissions at https://calendar.google.com. It’s the “Share this Calendar” tab in the settings menu:

squash3

So we are now ready to use the service account to actually read and update my calendar. I use Microsoft Visual Studio, and always code in C#. I’m not a developer, and the code isn’t pretty, but it works!

Click here to go the code at the end of this post

The new application is more lines of code than the simple Bash script, but it actually does a lot more. It checks my calendar every 10 minutes for unbooked Squash events that are within the next 7 days. If it detects one of those, it tries to book that court. This can be useful if I just want to book a court that’s available for a quiet time of day. I can also cancel courts really easily by simply appending the word “cancel” to the event title in the Google Calendar. The app then tries to cancel the court that I’ve got booked for that time, and deletes the event from my calendar if successful. It also tries to book the other court, if court 1 is not available.

The app still has a midnight mode where it runs 15 seconds before midnight. This grabs the Squash event from my calendar on the correct day, reads to see which time I’d like, maps it to the time code, calculates the day code for the online booking application, and attempts to book the court every second until it fails or gets a successful booking confirmation. If I get a successful response from the online booking system, then it updates my calendar with a booked title. It manages to book the court within a second of them becoming available, so it’s much faster than would be humanly possible using the web app. This is the .Net Google calendar application running:

squash4

One big advantage is that I can simply create Squash events on my phone, and as long as it’s more than a week in advance, I’m guaranteed to get the court. Happy hacker!

squash5

Future development

If you spot anything in my code that’s really really bad, please let me know! I’m sure there are better ways of doing things, but it’s a simple app that manages to do what it needs to do. One thing I could fix is the use of System.Windows.Forms.Timer. I should probably move that to System.Timers.Timer, and that would probably eliminate the need to check for clock drift, but I didn’t realise the difference when I started. I’d probably still need to check for daylight saving changes, so I’d need similar code anyway, and I don’t need it to be that accurate. I’d also love to move to push notifications from the Google Calendar API rather than a poll every 10 minutes. Future development!

That C# code

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Web;
using System.Net;
using Google.Apis.Auth.OAuth2;
using System.Threading;
using Google.Apis.Util.Store;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Services;
using System.IO;
using System.Security.Cryptography.X509Certificates;namespace SquashCalendarManager
{
public partial class Form1 : Form
{
CalendarService service;
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();private int periodSeconds = 5;
private int midnightSeconds = 100;string court = “0”;//Default to court 1.
const int Jan1stDay = 193;Dictionary<string, string> times = new Dictionary<string, string>()
{
{“06:20″,”0”},{“07:00″,”1”},{“07:40″,”2”},{“08:20″,”3”},{“09:00″,”4”},{“09:40″,”5”},{“10:20″,”6”},
{“11:00″,”7”},{“11:40″,”8”},{“12:20″,”9”},{“13:00″,”10”},{“13:40″,”11”},{“14:20″,”12”},
{“15:00″,”13”},{“15:40″,”14”},{“16:20″,”15”},{“17:00″,”16”},{“17:40″,”17”},{“18:20″,”18”},
{“19:00″,”19”},{“19:40″,”20”},{“20:20″,”21”},{“21:00″,”22”},{“21:40″,”23”},{“22:20″,”24″}
};private void SetUpMidnightTimer(TimeSpan alertTime)
{
DateTime current = DateTime.Now;
TimeSpan timeToGo = alertTime – current.TimeOfDay;
midnightSeconds=(int)timeToGo.TotalSeconds;
}

public Form1()
{
InitializeComponent();
string[] scopes = new string[] {
CalendarService.Scope.Calendar
};

var certificate = new X509Certificate2(@”GoogleSquashBooker.p12”, “notasecret”, X509KeyStorageFlags.Exportable);

textBoxOutput.AppendText(“Creating service…”);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(“[email protected]”)
{
Scopes = scopes
}.FromCertificate(certificate)
);

// Create the service.
service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = “GoogleSquashBooker”,
});
textBoxOutput.AppendText(“done.\r\n”);

SetUpMidnightTimer(new TimeSpan(23, 59, 45));

labelPeriodSeconds.Text = periodSeconds.ToString();
labelMidnightSeconds.Text = midnightSeconds.ToString();
timer.Interval = 1000; //miliseconds
timer.Tick += new EventHandler(PeriodTick);
timer.Enabled = true;
}

private void PeriodTick(object sender, EventArgs e)
{
periodSeconds–;
labelPeriodSeconds.Text = periodSeconds.ToString();
midnightSeconds–;
labelMidnightSeconds.Text = midnightSeconds.ToString();

if (midnightSeconds == 500 || midnightSeconds==4000)//check for clock skew 500 seconds before we think it’s midnight (and 4000 seconds to check for clock changes).
{
int oldTime = midnightSeconds;
SetUpMidnightTimer(new TimeSpan(23, 59, 45));
textBoxOutput.AppendText(“Midnight clock skew amount: “+(midnightSeconds-oldTime)+”\r\n”);
}

if (periodSeconds < 1)
{
periodSeconds = 600;
processPeriodBookings();
}
if (midnightSeconds < 1) { midnightSeconds = 86400;//reset clock 24 hours to avoid any more ticks over the next few seconds. processMidnightBookings(); } } private Events getEvents(int minDays, int maxDays) { EventsResource.ListRequest request = service.Events.List(“mycalendar”); request.TimeMin = DateTime.Now.AddDays(minDays); request.TimeMax = DateTime.Now.AddDays(maxDays); request.ShowDeleted = false; request.SingleEvents = true; request.MaxResults = 100; request.Q = “Squash”;//search term request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime; return request.Execute(); } private void processMidnightBookings() { textBoxOutput.AppendText(“=====================================================\r\n”); textBoxOutput.AppendText(“Time now: ” + DateTime.Now + “\r\n”); textBoxOutput.AppendText(“Getting midnight events…”); Events events = getEvents(7, 8); textBoxOutput.AppendText(“done.\r\n”); processEvents(events); } private void processPeriodBookings() { textBoxOutput.AppendText(“~~~~~~~~~~~~~~~~\r\n”); textBoxOutput.AppendText(“Time now: ” + DateTime.Now+”\r\n”); textBoxOutput.AppendText(“Getting this weeks events…”); Events events = getEvents(0, 7); textBoxOutput.AppendText(“done.\r\n”); processEvents(events); } private void processEvents(Events events) { textBoxOutput.AppendText(“Processing events…\r\n”); if (events.Items != null && events.Items.Count > 0)
{
foreach (Event eventItem in events.Items)
{
if (eventItem.Summary.ToLower().Contains(“squash”) && !eventItem.Summary.ToLower().Contains(“booked”) && eventItem.Start.DateTime != null)//Squash, not booked, not an all day event
{
DateTime eventDate = (DateTime)eventItem.Start.DateTime;
string eventTime = eventDate.ToString(“HH:mm”);
textBoxOutput.AppendText(eventItem.Summary + ” (“+ eventDate.ToString()+”)\r\n”);

if (eventItem.Summary.ToLower().Contains(“cancel”))
{
bool cancelSucessful = cancelCourt(“0”, getBookingTimeCode(eventTime), getBookingDayCode(eventDate)) || cancelCourt(“1”, getBookingTimeCode(eventTime), getBookingDayCode(eventDate));//Try to cancel court 2 if court 1 fails.
if (cancelSucessful)
{
service.Events.Delete(“mycalendar”, eventItem.Id).Execute();
textBoxOutput.AppendText(“Cancelled court\r\n”);
}
else
{
textBoxOutput.AppendText(“Cancel FAILED\r\n”);
string title = eventItem.Summary;
title = title.Replace(” FAILED”, “”);
title += ” FAILED”;
eventItem.Summary = title;
service.Events.Update(eventItem, “mycalendar”, eventItem.Id).Execute();
}
}
else {//book a new court
bool bookingSucessful = bookCourt(court, getBookingTimeCode(eventTime), getBookingDayCode(eventDate));
string title = eventItem.Summary;
title = title.Replace(” FAILED”, “”);

if (bookingSucessful)
{
textBoxOutput.AppendText(“Booked sucessfully :-)\r\n”);
title += ” booked :-)”;
}
else
{
textBoxOutput.AppendText(“Booking FAILED\r\n”);
title += ” FAILED”;
}
eventItem.Summary = title;
service.Events.Update(eventItem, “mycalendar “, eventItem.Id).Execute();
textBoxOutput.AppendText(“Event updated.\r\n”);
}
}
}
}
textBoxOutput.AppendText(“Done.\r\n”);
}

private string getBookingTimeCode(string time)
{
string timecode = “”;
try
{
timecode = times[time];
}
catch { }
return timecode;
}

private string getBookingDayCode(DateTime courtDate)
{
return (courtDate.Subtract(new DateTime(2016, 01, 01)).Days + Jan1stDay).ToString();
}

private bool bookCourt(string court, string time, string day)
{
string response = “”;
int attempt = 0;
while (attempt++ < 40 && !response.Contains(“Reserved”) && isValid(court) && isValid(time) && isValid(day)) { textBoxOutput.AppendText(“Attempting to book court “+court+” at “+time+” on day “+day+”. Attempt: (” + attempt+”)\r\n”); response = getBookingResponse(court, time, day); if (!response.Contains(“Reserved”)){ System.Threading.Thread.Sleep(1000); court = (court == “0” ? “1” : “0”);//alternate the court } } return response.Contains(“Reserved”); } private bool isValid(string item) { return item != null && item.Length > 0;
}

private string getBookingResponse(string court, string time, string day)
{
String responseString;
System.Net.ServicePointManager.Expect100Continue = false;
var request = (HttpWebRequest)HttpWebRequest.Create(“http://www.example.com/cgi-bin/scheduling/squashclub/schedule.cgi”);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.ProtocolVersion = HttpVersion.Version10;
request.KeepAlive = false;
request.ServicePoint.Expect100Continue = false;
request.ServicePoint.MaxIdleTime = 5000;
request.UserAgent = @”Mozilla/5.0 (Windows NT 10.0; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0″;
request.Method = “POST”;

string postData = @”selected_current_column=0&selected_current_court=” + court + @”&path=http%3A%2F%2Fwww.example.com%2Fcgi-bin%2Fscheduling%2Fsquashclub%2Fschedule.cgi&selection=” + time + @”&general_password=Open&mc=Open&selected_day=” + day + @”&[email protected]&password=mypassword&general_password=Open”;
ASCIIEncoding ascii = new ASCIIEncoding();
byte[] postBytes = ascii.GetBytes(postData.ToString());

request.ContentType = “application/x-www-form-urlencoded”;
request.ContentLength = postBytes.Length;

Stream dataStream = request.GetRequestStream();
dataStream.Write(postBytes, 0, postBytes.Length);
WebResponse response = request.GetResponse();

using (Stream stream = response.GetResponseStream())
{
StreamReader reader = new StreamReader(stream, Encoding.UTF8);
responseString = reader.ReadToEnd();
stream.Close();
}
response.Close();
return responseString;
}

private bool cancelCourt(string court, string time, string day)
{
String responseString;
System.Net.ServicePointManager.Expect100Continue = false;
var request = (HttpWebRequest)HttpWebRequest.Create(“http://www.example.com/cgi-bin/scheduling/squashclub/schedule.cgi”);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.ProtocolVersion = HttpVersion.Version10;
request.KeepAlive = false;
request.ServicePoint.Expect100Continue = false;
request.ServicePoint.MaxIdleTime = 5000;
request.UserAgent = @”Mozilla/5.0 (Windows NT 10.0; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0″;
request.Method = “POST”;

string cancelData = @”user_wants=Delete&[email protected]&selected_current_court=” + court + @”&path=http%3A%2F%2Fwww.example.com%2Fcgi-bin%2Fscheduling%2Fsquashclub%2Fschedule.cgi&selection=” + time + @”&general_password=Open&mc=&selected_day=” + day + @”&password=mypassword&general_password=Open”;

ASCIIEncoding ascii = new ASCIIEncoding();
byte[] postBytes = ascii.GetBytes(cancelData.ToString());

request.ContentType = “application/x-www-form-urlencoded”;
request.ContentLength = postBytes.Length;

Stream dataStream = request.GetRequestStream();
dataStream.Write(postBytes, 0, postBytes.Length);
WebResponse response = request.GetResponse();

using (Stream stream = response.GetResponseStream())
{
StreamReader reader = new StreamReader(stream, Encoding.UTF8);
responseString = reader.ReadToEnd();
stream.Close();
}
response.Close();
return responseString.Contains(“Successfully cancelled”);
}

private void buttonMidnight_Click(object sender, EventArgs e)
{
processMidnightBookings();
}

private void buttonProcessPeriodBookings_Click(object sender, EventArgs e)
{
processPeriodBookings();
}
}
}