Country Finder - NRF24CTF

kurtnettlekurtnettle
6 min read

Problem Statement

To be a good cybersecurity engineer you have to learn writing script in python.


Prerequisites

Solution

After connecting to the netcat instance, we will be given a list of coordinates. A very straightforward problem where we just need to perform reverse geocoding. So, we now need to

  • Parse the output to get the latitude and longitude.

  • Convert the coordinates to a country name (a.k.a reverse geocode).

output of netcat

Talking with netcat

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

addr = "chall.cbctf.xyz 34xxx"
server_ip, server_port = addr.split()

client.connect((server_ip, int(server_port)))

Before we start communicating with netcat, we need to create a socket instance and connect to the server using the provided address.

Recieving Message

We have a recv() function that receives data from the socket.

data = client.recv(2048) # buffer size = 2048 bytes
# also when you get a empty data it indicates
# that the client got disconnected (we will need this later :D)

# as we recieved bytes, we need to decode it
msg = data.decode()

We successfully read the output programmatically! 🥳

Hold on, We noticed that we will be given multiple rounds. How do we handle this?!

while True:
    try:
        data = client.recv(2048)
        msg = data.decode("utf-8")
        if len(msg) == 0:
            break
    except ConnectionResetError:
        break

We wrapped it in a while loop with a try-except block because when the connection is reset, it throws a ConnectionResetError exception.

Parsing Output

Now we need to parse the output. It's very simple since there is no complex formatting.

def parse_output(msg):
    for i in msg.split("\n"):
        x = i.split(",")
        if len(x) != 2:
            continue

        lat = x[0].split(":")[1].strip()
        lon = x[1].split(":")[1].strip()
  • We split the lines by \n

  • In each line, the latitude and longitude are separated by a comma, so we split it by ,

  • The values of latitude and longitude are separated by a colon, so we split them again by ,

Reverse Geocode

This service is offered by many providers. I initially chose GeoNames.

💡
GeoNames, a widely used API in custom ROMs, is used to get location data for weather report.

(What does this have to do with the write-up? Well, I learned about GeoNames from there)

However, it returned 404 errors for various coordinates, so I searched online and found LocationIQ and OpenCage

from requests import get


def fallback_0(lat, lon):
    api = (
        f"http://api.geonames.org/countryCodeJSON?lat={lat}&lng={lon}&username=[USERNAME]"
    )
    try:
        resp = get(api)
        data = resp.json()
        return data["countryName"]
    except:
        return False


def fallback_1(lat, lon):
    api = f"https://api.opencagedata.com/geocode/v1/json?q={lat}%2C{lon}&key=[API_KEY]"
    try:
        resp = get(api)
        data = resp.json()
        return data["results"][0]["components"]["country"]
    except:
        return False


def fallback_2(lat, lon):
    api = f"https://us1.locationiq.com/v1/reverse?lat={lat}&lon={lon}&format=json&zoom=3&accept-language=en&key=[API_KEY]"
    try:
        resp = get(api)
        data = resp.json()
        return data["address"]["country"]
    except:
        return False

I added multiple fallback APIs because I was getting 404 earlier (just to be safe).

Combining the API with the parsed output

def latlon_2_cn(lat, lon):
    cn = fallback_1(lat, lon)
    if not cn:
        print(f"falling back to 2 - {lat},{lon}")
        cn = fallback_2(lat, lon)
    if not cn:
        print(f"failed to get country name - {lat},{lon}")
        input("enter the country name manually:")

    return cn


def handle_output(msg):
    cns = []
    for i in msg.split("\n"):
        x = i.split(",")
        if len(x) != 2:
            continue

        lat = x[0].split(":")[1].strip()
        lon = x[1].split(":")[1].strip()
        cn = latlon_2_cn(lat, lon)
        if cn:
            cns.append(cn)
        else:
            print(f"{lat},{lon} - country not found")
            cn = input("Enter the country name: ")
            if not cn:
                break

    cns = sorted(cns)
    return ",".join(cns)

We have multiple rounds, typically 21-28, and it would be terrible if your script fails to get a country name in the middle of it. So, I added an input() as a last resort.

Sending message

Just as we have a recv() function to receive data from the socket, we also have a send() function to send messages to the socket. Remember, we can only send bytes, so we need to encode() the message first.

msg = "my msg"
data = msg.encode()
client.send(data)

Alright, we have covered all the parts, so let's put all the pieces together.

Source Code

apis.py

from requests import get


def fallback_0(lat, lon):
    api = (
        f"http://api.geonames.org/countryCodeJSON?lat={lat}&lng={lon}&username=[USERNAME]"
    )
    try:
        resp = get(api)
        data = resp.json()
        return data["countryName"]
    except:
        return False


def fallback_1(lat, lon):
    api = f"https://api.opencagedata.com/geocode/v1/json?q={lat}%2C{lon}&key=[API_KEY]"
    try:
        resp = get(api)
        data = resp.json()
        return data["results"][0]["components"]["country"]
    except:
        return False


def fallback_2(lat, lon):
    api = f"https://us1.locationiq.com/v1/reverse?lat={lat}&lon={lon}&format=json&zoom=3&accept-language=en&key=[API_KEY]"
    try:
        resp = get(api)
        data = resp.json()
        return data["address"]["country"]
    except:
        return False

db.py

import sqlite3


class LocationDB:
    def __init__(self):
        self.conn = sqlite3.connect("locations.db")
        self.cursor = self.conn.cursor()

        self.cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS locations (
            lat TEXT,
            lon TEXT,
            cn TEXT)
        """
        )

    def put(self, lat, lon, cn):
        self.cursor.execute(
        """
        INSERT INTO locations (lat, lon, cn)
        VALUES (?, ?, ?)
        """,
            (lat, lon, cn)
        )
        self.conn.commit()

    def get(self, lat, lon):
        self.cursor.execute(
        """
        SELECT * FROM locations
        WHERE lat = ? AND lon = ?
        """,
            (lat, lon)
        )

        res = self.cursor.fetchone()
        if res:
            return res[2]

main.py

import socket

from apis import *
from db import LocationDB

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
db = LocationDB()

addr = "chall.cbctf.xyz xxxxx"
server_ip, server_port = addr.split()
client.connect((server_ip, int(server_port)))


def latlon_2_cn(lat, lon):
    cn = db.get(lat, lon)
    if cn:
        print(f"found in cache - {lat},{lon},{cn}")
    # if not cn:
    #     print("falling back to 0")
    #     cn = fallback_0(lat,lon)
    if not cn:
        print(f"falling back to 1 - {lat},{lon}")
        cn = fallback_1(lat, lon)
    if not cn:
        print(f"falling back to 2 - {lat},{lon}")
        cn = fallback_2(lat, lon)
    if not cn:
        print(f"failed to get country name - {lat},{lon}")

    if cn:
        db.put(lat, lon, cn)
        return cn


def handle_output(msg):
    cns = []
    for i in msg.split("\n"):
        x = i.split(",")
        if len(x) != 2:
            continue

        lat = x[0].split(":")[1].strip()
        lon = x[1].split(":")[1].strip()
        cn = latlon_2_cn(lat, lon)
        if cn:
            cns.append(cn)
        else:
            print(f"{lat},{lon} - country not found")
            cn = input("Enter the country name: ")
            if not cn:
                break

    cns = sorted(cns)
    return ",".join(cns)


if __name__ == "__main__":
    while True:
        try:
            msg = client.recv(2048).decode("utf-8")
            print(msg)
            if len(msg) == 0:
                break
        except ConnectionResetError:
            break
        finally:
            client.close()

        msg = handle_output(msg)
        print(msg)
        data = f"{msg}\n".encode("utf-8")
        client.send(data)

Conclusion

  • I tried sharing all of my thought process because I love to read about how other people think about problems more than just seeing the straightforward solution.

  • Thanks to my coding skills, I had to add a cache system because the API service allows only a limited number of requests (2.5k/day). If I keep debugging my issues on production, I would need to create a dozen of accounts :)

  • Databases might sound daunting, but the project I am currently working on had a lot of database work, so it was no big issue for me to integrate it.

  • During the contest, I couldn't submit my script because I forgot about socket programming and was googling it. My teammates were quick and submitted their script while I was still debugging my script.

Credits

  • Hashnode - for the amazing platform

  • GeoNames, OpenCage, LocationIQ - for their free-tier API

  • EWU - for delaying this write-up (they restricted challenge access after <36 hours)

0
Subscribe to my newsletter

Read articles from kurtnettle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

kurtnettle
kurtnettle