top of page

Tracking GPS Locations From Digital Photos (Guide)

Tracking GPS Locations From Digital Photos (Guide) | Black Hat HQ

Tracking GPS Locations From Digital Photos


GPS extraction from EXIF data is standard OSINT methodology. Here's the complete technical breakdown. This is an article on tracking GPS locations from digital photos.


How It Works


Digital photos store metadata in EXIF (Exchangeable Image File Format) tags embedded in JPEG, TIFF, and RAW files. Smartphones with GPS populate these tags automatically:


EXIF Tag

Value

GPSLatitude

Degrees, minutes, seconds

GPSLatitudeRef

N or S

GPSLongitude

Degrees, minutes, seconds

GPSLongitudeRef

E or W

GPSAltitude

Meters above sea level

GPSTimeStamp

UTC time of GPS fix

GPSDateStamp

Date of GPS fix

GPSImgDirection

Compass bearing of camera

GPSProcessingMethod

Often reveals device type


These tags persist through basic edits, email attachments, and file uploads unless explicitly stripped.


Extraction Tools


exiftool (The Gold Standard)


bash

# Install
sudo apt install exiftool

# Dump all metadata
exiftool photo.jpg

# Extract only GPS tags
exiftool -GPS* photo.jpg

# Machine-readable output (decimal degrees)
exiftool -c "%.6f" -GPSLatitude -GPSLongitude -GPSAltitude photo.jpg

# Extract GPS from every photo in a directory
exiftool -r -GPS* -c "%.6f" /path/to/photos/

# Output as CSV
exiftool -r -csv -GPSLatitude -GPSLongitude -GPSAltitude -GPSDateStamp /path/to/photos/ > gps_data.csv

# Create a KML file for Google Earth
exiftool -r -p '<Placemark><name>%f</name><Point><coordinates>${gpslongitude#},${gpslatitude#}</coordinates></Point></Placemark>' -ext jpg /path/to/photos/ > photos.kml

Alternative Tools


bash

# identify (ImageMagick)
identify -verbose photo.jpg | grep -i gps

# strings + grep (quick and dirty, find raw GPS data)
strings photo.jpg | grep -A5 -B5 "GPS"

# jhead (lightweight, fast)
jhead photo.jpg

# exiv2 (C++ library with CLI)
exiv2 -P E photo.jpg

Python with PIL/Pillow


python

from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
import sys

def decimal_coords(coords, ref):
    """Convert EXIF GPS to decimal degrees"""
    decimal = coords[0] + coords[1] / 60.0 + coords[2] / 3600.0
    if ref in ['S', 'W']:
        decimal = -decimal
    return decimal

def extract_gps(filepath):
    img = Image.open(filepath)
    exif_data = img._getexif()
    
    if not exif_data:
        return None
    
    gps_info = {}
    for tag_id, value in exif_data.items():
        tag = TAGS.get(tag_id, tag_id)
        if tag == "GPSInfo":
            for gps_tag_id in value:
                gps_tag = GPSTAGS.get(gps_tag_id, gps_tag_id)
                gps_info[gps_tag] = value[gps_tag_id]
    
    if not gps_info:
        return None
    
    lat = decimal_coords(gps_info["GPSLatitude"], gps_info["GPSLatitudeRef"])
    lon = decimal_coords(gps_info["GPSLongitude"], gps_info["GPSLongitudeRef"])
    altitude = gps_info.get("GPSAltitude", None)
    timestamp = f"{gps_info.get('GPSDateStamp', 'Unknown')} {gps_info.get('GPSTimeStamp', 'Unknown')}"
    
    return {
        "latitude": lat,
        "longitude": lon,
        "altitude": altitude,
        "timestamp": timestamp
    }

if __name__ == "__main__":
    gps = extract_gps(sys.argv[1])
    if gps:
        print(f"Latitude:  {gps['latitude']:.6f}")
        print(f"Longitude: {gps['longitude']:.6f}")
        print(f"Altitude:  {gps['altitude']}")
        print(f"Timestamp: {gps['timestamp']}")
    else:
        print("No GPS data found.")

Bulk Extraction Script


For an engagement where you've collected photos from a target (authorized, per your scope):


python

#!/usr/bin/env python3
"""Bulk GPS extraction for pentest photo analysis"""
import os
import json
import csv
from datetime import datetime
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS

def decimal_from_dms(dms, ref):
    degrees, minutes, seconds = dms
    decimal = float(degrees) + float(minutes) / 60.0 + float(seconds) / 3600.0
    if ref in ('S', 'W'):
        decimal = -decimal
    return decimal

def extract_all_metadata(filepath):
    """Extract GPS and all useful EXIF from a single image"""
    img = Image.open(filepath)
    exif = img._getexif()
    if not exif:
        return None
    
    result = {"filename": os.path.basename(filepath), "filepath": filepath}
    
    for tag_id, value in exif.items():
        tag = TAGS.get(tag_id, tag_id)
        
        if tag == "GPSInfo":
            gps = {}
            for gps_id in value:
                gps_tag = GPSTAGS.get(gps_id, gps_id)
                gps[gps_tag] = value[gps_id]
            
            if "GPSLatitude" in gps and "GPSLongitude" in gps:
                result["gps_lat"] = decimal_from_dms(gps["GPSLatitude"], gps.get("GPSLatitudeRef", "N"))
                result["gps_lon"] = decimal_from_dms(gps["GPSLongitude"], gps.get("GPSLongitudeRef", "E"))
                result["gps_altitude"] = gps.get("GPSAltitude", None)
                result["gps_timestamp"] = f"{gps.get('GPSDateStamp', '')} {gps.get('GPSTimeStamp', '')}"
            
        elif tag == "Make":
            result["camera_make"] = str(value)
        elif tag == "Model":
            result["camera_model"] = str(value)
        elif tag == "DateTimeOriginal":
            result["photo_taken"] = str(value)
        elif tag == "Software":
            result["software"] = str(value)
    
    return result if "gps_lat" in result else None

def walk_directory(root_path, output_csv="gps_findings.csv"):
    """Recursively scan directory for images with GPS data"""
    results = []
    
    for dirpath, _, filenames in os.walk(root_path):
        for f in filenames:
            if f.lower().endswith(('.jpg', '.jpeg', '.tiff', '.tif', '.png', '.heic')):
                fullpath = os.path.join(dirpath, f)
                try:
                    data = extract_all_metadata(fullpath)
                    if data:
                        results.append(data)
                        print(f"[+] GPS found: {f} → {data['gps_lat']:.6f}, {data['gps_lon']:.6f}")
                except Exception as e:
                    print(f"[-] Error processing {f}: {e}")
    
    # Write CSV
    if results:
        fieldnames = ["filename", "gps_lat", "gps_lon", "gps_altitude", 
                      "gps_timestamp", "photo_taken", "camera_make", 
                      "camera_model", "software", "filepath"]
        
        with open(output_csv, 'w', newline='') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore')
            writer.writeheader()
            writer.writerows(results)
        
        print(f"\n[+] Wrote {len(results)} GPS-tagged photos to {output_csv}")
        
        # Generate Google Maps URL for each
        print("\n[+] Google Maps Links:")
        for r in results:
            print(f"    {r['filename']}: https://maps.google.com/?q={r['gps_lat']},{r['gps_lon']}")
    else:
        print("[!] No GPS-tagged photos found.")
    
    return results

if __name__ == "__main__":
    import sys
    target_dir = sys.argv[1] if len(sys.argv) > 1 else "."
    walk_directory(target_dir)

What GPS Actually Reveals in a Pentest


Beyond just coordinates, you can pivot on this data:


From a photo taken at coordinates (X, Y):


  • Physical reconnaissance: The target's home, office, or frequented locations. Cross-reference with property records, business registrations, social media check-ins.

  • Movement patterns: Multiple photos create a timeline of movement over days/weeks — commute routes, travel patterns, routines.

  • Security infrastructure: Photos taken outside buildings reveal security cameras, access control, guard positions, and physical layout.

  • Geolocation pivoting: Coordinates → reverse geocoding → address → property records → owner names → additional OSINT.

  • Proximity analysis: "Who else took photos at these coordinates on these dates?" — social graph reconstruction.


Reverse Geocoding After Extraction


bash

# Using Nominatim (OpenStreetMap, free, rate-limited)
curl -s "https://nominatim.openstreetmap.org/reverse?format=json&lat=51.5074&lon=-0.1278" | jq .

# Bulk reverse geocoding
for lat in $(awk -F, '{print $2}' gps_findings.csv | tail -n+2); do
    lon=$(awk -F, '{print $3}' gps_findings.csv | tail -n+2)
    curl -s "https://nominatim.openstreetmap.org/reverse?format=json&lat=$lat&lon=$lon&zoom=18" | \
        jq -r '.display_name'
    sleep 1  # Nominatim limit: 1 req/sec
done

Or with Python + geopy:


python

from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter

geolocator = Nominatim(user_agent="pentest_osint")
reverse = RateLimiter(geolocator.reverse, min_delay_seconds=1.1)

coordinates = [(51.5074, -0.1278), (40.7128, -74.0060)]
for lat, lon in coordinates:
    location = reverse(f"{lat}, {lon}")
    print(f"{lat}, {lon}: {location.address}")

Photos That Won't Have GPS


When photos come back empty, here's why:


  • Screenshots: Nearly never contain GPS (screen capture, not camera capture)

  • DSLR/mirrorless cameras: Most don't have GPS modules unless tethered or using an external GPS accessory

  • Stripped by platform: Facebook, Twitter, Instagram, WhatsApp, and most messaging apps strip EXIF on upload (notable exception: email attachments often survive, and some forums/image hosts don't strip)

  • Edited and re-saved: Some editors preserve EXIF, some strip it, some corrupt it

  • Taken with location services off: iOS and Android both allow disabling camera location

  • PNG files: EXIF support in PNG is inconsistent — most PNGs from screenshots lack GPS


Defense Testing Angle


If you're pentesting a client who publishes photos (corporate website, blog, social media, job postings), you're testing for:


  • Are employees posting photos from inside secure facilities with GPS enabled?

  • Do marketing photos of the "office interior" reveal GPS to the building's exact coordinates, which is also the datacenter location?

  • Do job posting photos on LinkedIn show the secure facility's layout and GPS coordinates?

  • Has EXIF stripping been implemented in publishing workflows?


Testing script for web-based photos:


bash

#!/bin/bash
# Download and scan all images from a target URL for GPS EXIF

TARGET_URL="$1"
TEMP_DIR=$(mktemp -d)

# Scrape all image URLs
lynx -dump -listonly -image_links "$TARGET_URL" | \
    grep -iE '\.(jpg|jpeg|png|gif|webp|tiff)' | \
    awk '{print $2}' | sort -u > "$TEMP_DIR/urls.txt"

# Download them
cd "$TEMP_DIR"
wget -q -i urls.txt -P images/

# Scan for GPS
exiftool -r -quiet -GPSLatitude -GPSLongitude -GPSDateStamp images/

# Cleanup
rm -rf "$TEMP_DIR"

Google Maps / Earth Visualization


After extracting coordinates, drop them on a map for the report:


bash

# Generate a KML file for Google Earth
exiftool -r -p '
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
$filenames
</Document>
</kml>
' -d '%Y-%m-%d %H:%M:%S' \
-p '<Placemark>
  <name>$filename</name>
  <description>Date: $gpsdatetime</description>
  <Point>
    <coordinates>$gpslongitude#,$gpslatitude#,0</coordinates>
  </Point>
</Placemark>' \
-ext jpg /path/to/photos/ > findings.kml

Open findings.kml in Google Earth Pro and you'll have every photo plotted geographically with timestamps. This makes a powerful visual for the pentest report.


Enroll In Online Cybersecurity & Hacking Classes/Courses | Black Hat HQ

Comments


Master the Art!

Info

715-527-1928

www.blackhathq.com

Address

P.O. Box 126
Antigo, Wisconsin 54409

The skills/techniques/guides on this site are not for illegal/illicit use and are not condoned by
Black Hat HQ!

Best Value

Elite Hacker

$100

100

Every month

Get Access To All The Courses For A Monthly Fee

Valid until canceled

Get complete access to all courses with Elite Hacker!

Get full access to exclusive online Groups/Forums!

Best Value

Neophyte

$50

50

Every month

Get Access To All Courses $10 Or Less!

Valid until canceled

Get access to all courses $10 or under!

Get exclusive access to specific forums/groups!

Choose your pricing plan

Find one that works for you

© 2026 Black Hat HQ

bottom of page