Solved: How to enable SSH in the WebGUI on a Ubiquiti Edgerouter with cURL (CLI/Scriptable)

Hey, so I’m trying to set up some scripts to automatically configure/rebuild my network from factory defaults. One of the very first obstacles is that my Edgerouter only has the webgui enabled by default. No ssh. So I’d like to log into the webgui and enable ssh, ideally with only curl.

I understand how this should work in a very abstract way. I send the login info with curl, parse cookies or session data or whatever out of the response, then send the equivalent of a check under ssh > enable + save button on the dashboard page with the session credentials.

From there, sending commands over ssh to configure the router is a piece of cake.

Anyone know how to do this? I have only tried the most basic:

curl -k -F "username=ubnt&password=ubnt" "https://192.168.1.1"

to which I get a 417 - Expectation Failed error page.

I have the headers of the legit response pulled up in Chrome Developer Tools, if any of that information helps…


There also appears to be an undocumented API, which I believe is how UNMS works.

EDGE.Config = {
            Api: {
                'base': 'https://192.168.1.1/api/',
                'get': 'https://192.168.1.1/api/edge/get.json',
                'set': 'https://192.168.1.1/api/edge/set.json',
                'delete': 'https://192.168.1.1/api/edge/delete.json',
                'batch': 'https://192.168.1.1/api/edge/batch.json',
                'data': 'https://192.168.1.1/api/edge/data.json',
                'heartbeat': 'https://192.168.1.1/api/edge/heartbeat.json',
                'setup': 'https://192.168.1.1/api/edge/setup.json',
                'feature': 'https://192.168.1.1/api/edge/feature.json'
            },
2 Likes

This succeeds (exits 0 at least), but no output?

curl -k -d 'username=ubnt&password=ubnt' -X POST https://192.168.1.1

Wow, look at that…

06%20AM

This results in a very long curl command with a bunch of headers and other things, which also silently succeeds. I tried adding -v which gives me a bunch of information, but no actual html.

Much further along now, but getting 400 - Bad Request. All of this is coming from deconstructing the stuff in Google Developer Tools under Network.

ROUTER_IP="192.168.1.1"

COOKIES="$(curl -v "https://${ROUTER_IP}/" \
-H 'Connection: keep-alive' \
-H 'Cache-Control: max-age=0' \
-H "Origin: https://${ROUTER_IP}" \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'DNT: 1' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' \
-H 'Sec-Fetch-Mode: navigate' \
-H 'Sec-Fetch-User: ?1' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \
-H 'Sec-Fetch-Site: same-origin' \
-H "Referer: https://${ROUTER_IP}/" \
-H 'Accept-Encoding: gzip, deflate, br' \
-H 'Accept-Language: en-US,en;q=0.9' \
--data 'username=ubnt&password=ubnt' \
--compressed \
--insecure 2>&1 | 
grep "^< Set-Cookie")"

PHP="$(echo "${COOKIES}" | grep "PHPSESSID" | sed 's/.*ID=//')"
TOKEN="$(echo "${COOKIES}" | grep "X-CSRF-TOKEN" | sed 's/.*TOKEN=//')"
BEAKER="$(echo "${COOKIES}" | grep "beaker.session.id" | sed 's/.*id=//' | cut -d ';' -f 1)"

curl "https://${ROUTER_IP}/api/edge/batch.json" \
-H 'Sec-Fetch-Mode: cors' \
-H "Origin: https://${ROUTER_IP}" \
-H 'Accept-Encoding: gzip, deflate, br' \
-H "X-CSRF-TOKEN: ${TOKEN}" \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'X-Requested-With: XMLHttpRequest' \
-H "Cookie: PHPSESSID=${PHP}; X-CSRF-TOKEN=${TOKEN}; beaker.session.id=${BEAKER}" \
-H 'Connection: keep-alive' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/javascript, */*; q=0.01' \
-H "Referer: https://${ROUTER_IP}/" \
-H 'Sec-Fetch-Site: same-origin' \
-H 'DNT: 1' \
--data-binary $'{"SET":{"service":{"ssh":"\'\'"}},"GET":{"system":null,"layer2":null,"service":null}}' \
--compressed \
--insecure
1 Like

wtf…

$ echo "${TOKEN}"
blahblahblahblahblahblahblahblahblahblahblahblahblahblahblah
$ echo "${TOKEN} and something else"
 and something elseblahblahblahblahblahblahblahblahblahblahblahblahblahblahblah

Never seen that before…


Hmmm…

$ declare | grep ^TOKEN
TOKEN=$'blahblahblahblahblahblahblahblahblahblahblahblahblahblahblah\r'
$ thing=fun
$ declare | grep ^thing
thing=fun

I do not know the implication of $'stuff'.


Oh it’s just cause it has escape characters in it. Going to get rid of it with a grep -o "[[:alnum:]]*", but if there’s a more elegant way to declare those cookie variables in the middle, I’m all ears.

Ok, I did it. It works. I’m sure it could be simplified quite a bit, but this does work:

ROUTER_IP="192.168.1.1"

COOKIES="$(curl -v "https://${ROUTER_IP}/" \
-H 'Connection: keep-alive' \
-H 'Cache-Control: max-age=0' \
-H "Origin: https://${ROUTER_IP}" \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'DNT: 1' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' \
-H 'Sec-Fetch-Mode: navigate' \
-H 'Sec-Fetch-User: ?1' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \
-H 'Sec-Fetch-Site: same-origin' \
-H "Referer: https://${ROUTER_IP}/" \
-H 'Accept-Encoding: gzip, deflate, br' \
-H 'Accept-Language: en-US,en;q=0.9' \
--data 'username=ubnt&password=ubnt' \
--compressed \
--insecure 2>&1 | 
grep "^< Set-Cookie")"

PHP="$(echo "${COOKIES}" | grep "PHPSESSID" | sed 's/.*ID=//' | grep -o "[[:alnum:]]*" )"
TOKEN="$(echo "${COOKIES}" | grep "X-CSRF-TOKEN" | sed 's/.*TOKEN=//' | grep -o "[[:alnum:]]*" )"
BEAKER="$(echo "${COOKIES}" | grep "beaker.session.id" | sed 's/.*id=//' | cut -d ';' -f 1)"

curl "https://${ROUTER_IP}/api/edge/batch.json" \
-H 'Sec-Fetch-Mode: cors' \
-H "Origin: https://${ROUTER_IP}" \
-H 'Accept-Encoding: gzip, deflate, br' \
-H "X-CSRF-TOKEN: ${TOKEN}" \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'X-Requested-With: XMLHttpRequest' \
-H "Cookie: PHPSESSID=${PHP}; X-CSRF-TOKEN=${TOKEN}; beaker.session.id=${BEAKER}" \
-H 'Connection: keep-alive' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/javascript, */*; q=0.01' \
-H "Referer: https://${ROUTER_IP}/" \
-H 'Sec-Fetch-Site: same-origin' \
-H 'DNT: 1' \
--data-binary $'{"SET":{"service":{"ssh":"\'\'"}},"GET":{"system":null,"layer2":null,"service":null}}' \
--compressed \
--insecure
6 Likes

this is incredible. I hope search engines find this because bwahahahahahahahahaha this is nice.

5 Likes

if there’s a bot breaking into unsecured edgerouters in a few weeks, I won’t know whether to feel proud or guilty.

2 Likes

Trying this on OPNsense and I cannot get it to work. It handles the CSRF token with Ajax.

I can see it when I load the login page (before authenticating), and I can parse out the value.

<script>
  $( document ).ready(function() {
    $.ajaxSetup({
      'beforeSend': function(xhr) {
        xhr.setRequestHeader("X-CSRFToken", "blahblahblahblahbalh" );
      }
    });
  });
</script>

But adding the header in curl doesn’t register.

curl -v "https://${ROUTER_IP}/" \
...
-H "X-CSRFToken: blahblahblahblahbalh" \
...

produces

<html><head><title>CSRF check failed</title>
            <script>
              $( document ).ready(function() {
                  $.ajaxSetup({
                  'beforeSend': function(xhr) {
                      xhr.setRequestHeader("X-CSRFToken", "blahblahblahblahbalh" );
                  }
                });
              });
            </script>
            </head>
                  <body>
                  <p>CSRF check failed. Your form session may have expired, or you may not have cookies enabled.</p>
                  </body></html>

It seems to want to get the header after the page loads, in a way that might be too complex to accomplish with curl.

So I guess good job OPNsense on making it difficult to hack the web gui… kinda wish you’d have SSH on LAN by default, but I guess I can stand to do this manually.


Further reading:

1 Like

If you enable the cookie file to somewhere wireable it’ll work. The initial load before redirect initializes a session which is lost w/o the redirect unless you preserve cookies . I think.

1 Like

Yeah, I’m in over my head here. Going to focus on configuring things via ssh and/or api and not let this take up too much of my time. I’d like to come back to it though.