Analiza kodu webaplikacji zdecydowanie różni się od reverse engineeringu jaki znamy z analizy aplikacji desktopowych czy mobilnych. W przypadku webaplikacji nie mamy kodu backendu, więc musimy kierować się przesłankami na jego temat, które wynikają z danych wynikowych. Jedyny kod do analizy jaki otrzymujemy to JavaScript załączony do strony.

Na wstępie tego wpisu chcę zaznaczyć, że analizą webaplikacji zajmuję się jedynie hobbystycznie, nie mam profesjonalnego przygotowania i traktuję ją jako element poszerzania mojej wiedzy w dziedzinie web developmentu.

Analiza

Swoją analizę rozpocząłem od strony https://wroclawskirower.pl/mapa-stacji/, na której znajduje się mapa z naniesionymi informacjami o stacjach. Kiedy zobaczyłem tę stronę zainteresowało mnie, czy jest możliwość uzyskania informacji o stacjach w sposób umożliwiający ich dalsze przetwarzanie.

Już na pierwszy rzut oka widać, że mapa dostarczana jest przez Google Maps. Przyjąłem zatem proste założenie, że po załadowaniu mapy skrypt JavaScript będzie potrzebował danych o stacjach, które naniesie na mapę używając Google Maps API.

Szybki rzut oka na sekcję <head>:

<head>
<style type="text/css">
<link type="text/css" rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700">
<style type="text/css">
<style type="text/css">
<style type="text/css">
<meta charset="UTF-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=700" name="viewport">
<title>Mapa stacji | Wrocławski Rower Miejski</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js">
<style>
<style>
<link href="https://wroclawskirower.pl/wp-content/themes/okiTheme/assets/css/basic.css?v=20140613" rel="stylesheet">
<script>
<script src="https://nextbike.net/iframe/nextbike.js" type="text/javascript">
<script src="https://nextbike.net/api/nextbike.lib.js" type="text/javascript">
<script type="text/javascript">
<link type="image/x-icon" href="https://wroclawskirower.pl/wp-content/themes/okiTheme/assets/img/favicons/wrm.png" rel="shortcut icon">
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/common.js">
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/map.js">
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/util.js">
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/marker.js">
<style>
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/onion.js">
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/controls.js">
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/stats.js">
<script type="text/javascript" charset="UTF-8" src="https://maps.gstatic.com/maps-api-v3/api/js/20/11c/intl/pl_ALL/infowindow.js">
</head>

Informuje nas jedynie, że API map wczytywane jest zanim dokument zostanie złożony przez przeglądarkę. Wyszukałem zatem innych skryptów pod koniec dokumentu. Przeglądając kolejne skrypty natknąłem się na interesujący dokument. Moją uwagę zwrócił następujący fragment:

function placeNextBikeMarkers()
{
    var centerLon = 17.0327515, centerLat = 51.1101471, centerZoom = parseInt(NEXTBIKE_REGION_ZOOM);
    var nbArr, region, place, mTS, mC, center;
    var markerToShow = new Array();
    var markerCities = new Array();
    var i, j;
    
    try {
        nbArr = JSON.parse(NEXTBIKE_PLACES_DB);
    } catch(e) {
        nbArr = null;
    }
    
    if (nbArr===null)
        return;
    // POMINIĘTY FRAGMENT KODU
}

Z nazwy funkcji założyć można, że to ona odpowiada za umiejscowienie znaczników. Co więcej, pojawia się wywołanie parsera JSON. Parsuje on zawartość zmiennej NEXTBIKE_PLACES_DB, która jednak nie wystąpiła w analizowanym dokumencie. Powróciłem zatem do strony mapy i przeszukałem dokument w poszukiwaniu takiej zmiennej. Ta próba okazała się celnym strzałem, gdyż natrafiłem na następujący fragment:

<div class="box-with-map-container">
<script>

                        var NEXTBIKE_REGION_NAME = 'all';
                        var NEXTBIKE_REGION_ZOOM = '13';
                        var NEXTBIKE_PLACES_DB = '[{"region_info":{"lat":"52.2413","lng":"18.479","zoom":"6","name":"WRM nextbike Poland","hotline":"+48717381111","domain":"pl","country":"PL", 
            // fragment pominięty
</script>
<div id="mapa" class="box2" style="height: 580px; position: relative; background-color: rgb(229, 227, 223); overflow: hidden;">
</div>

Okazuje się zatem, że wszystkie dane dotyczące stacji wystawiane są do JavaScriptu przez skrypty backendowe z pomocą globalnej zmiennej. Teraz wystarczyło jedynie napisać prosty skrypt, który wyciągnie pożądane informacje. Przygotowałem proste api, które odczytuje informacje o stacji wskazanej przez zadany numer.

#!/usr/bin/python
#-*- coding: UTF-8 -*-
import cgi
import requests 
import json
from pyquery import PyQuery as pq

print "Content-Type: text/html;charset=utf-8\n\n"
print
f = cgi.FieldStorage()

n = f.getvalue("n", "5946") 

s = requests.Session(verify=False)

r = s.get("https://wroclawskirower.pl/mapa-stacji/")
d = pq(r.text.encode("UTF-8"))
jsonString = d(".box-with-map-container script").html().split("var NEXTBIKE_PLACES_DB =")[1].strip()[1:-2]
data = json.loads(jsonString)[0]

for p in data['places']:
    if p['number'] == n:
        print json.dumps(p)

Jak pokazuje przykład, API przekazuje nadmiarowe dane dotyczące stacji. Zestawienie nie przechowuje danych poufnych, jednak poza nazwą stacji czy ilością rowerów można odczytać numery rowerów, które znajdują się na stacji.