GitHub Capture the Flag results
Earlier this month, we challenged you to a Call to Hacktion—a CTF (Capture the Flag) competition to put your GitHub Workflow security skills to the test. Participants were invited to…
Earlier this month, we challenged you to a Call to Hacktion—a CTF (Capture the Flag) competition to put your GitHub Workflow security skills to the test.
Participants were invited to find a vulnerability in a dedicated private game repository by signing up via our incrediblysecureinc/ctf-registration repo. The goal of the game was to escalate read-only repo access to write access through a vulnerable GitHub workflow, as well as learn about GitHub Workflow privilege models and security considerations in a fun and interactive way.
All in all, nearly 350 players from across the GitHub community participated in the CTF with 54 players ultimately solving the game within the allotted time frame.
@Creastery was the first person to fully exploit the vulnerability with a recorded finishing time of 2021-03-17T20:40:07.366Z (UTC)—1 hour, 40 mins, and 7 seconds—after the details of the CTF were made public 🤯
@Creastery AKA Ngo Wei Lin is an active CTF player and security researcher, as is apparent from their rapid finish in our game. We’d like to take this opportunity to officially congratulate them on their first-place finish!
The solution
The core of the challenge revolved around a vulnerable GitHub Workflow in a player-dedicated private repository. Contestants were given read-only privileges to this repository, and the objective of the game was to find and exploit the Workflow vulnerability to obtain write privileges to the main branch.
name: log and process issue comments
on:
issue_comment:
types: [created]
jobs:
issue_comment:
name: log issue comment
runs-on: ubuntu-latest
steps:
- id: comment_log
name: log issue comment
uses: actions/github-script@v3
env:
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
with:
github-token: "deadc0de"
script: |
console.log(process.env.COMMENT_BODY) // line 20
return process.env.COMMENT_ID
result-encoding: string
- id: comment_process
name: process comment
uses: actions/github-script@v3
timeout-minutes: 1
if: ${{ steps.comment_log.outputs.COMMENT_ID }}
with:
script: |
const id = ${{ steps.comment_log.outputs.COMMENT_ID }} // line 30
return ""
result-encoding: string
It’s important to treat GitHub Workflow code like it is privileged code in the context of your repo, especially if it is handling untrusted input. GitHub Workflows often run with full-privileged access to the repository they are triggered from. This means you have to be extra careful when dealing with user-supplied input in your workflows.
The careful reader will have spotted a potential templated Javascript injection flaw on line 30, but what made this exercise a bit more challenging was figuring out how to actually trigger this injection vulnerability.
On line 20, we see that the vulnerable Workflow is logging a user controlled untrusted input (an issue comment) to stdout
via a call to console.log
. Yet how does that facilitate the injection vulnerability?
This is where knowledge of GitHub Workflow Commands comes into play. Unless explicitly disabled, a Workflow runner will filter stdout
looking for Workflow commands. Workflow commands, among other things, allow you to pass values between Workflow steps via the set-output command.
If you intend to log untrusted input to stdout
from the context of a GitHub Workflow, you first have to disable Workflow command processing. To securely disable Workflow commands, you have to use a dynamic and random token that can’t be predicted by an attacker (otherwise they would simply be able to re-enable command processing). GitHub deprecated some of the more security sensitive workflow commands in 2020, but in certain use patterns, logging untrusted input can still lead to surprises if not handled carefully.
In the challenge, the templated Javascript injection on line 30 was controlled by the output of a previous step (steps.comment_log.outputs.COMMENT_ID
). This practically meant that players could leave issue comments that controlled the contents of this templated output value, by leveraging the set-output Workflow command.
Let’s take a closer look at @Creastery’s solution. To solve the challenge, they posted the following comment:
This resulted in the following commit to the main branch of their challenge repository:
We can dissect the winning comment in a bit more detail to better understand what happened.
:set-output name=COMMENT_ID::1; console.log(context); console.log(process); await github.request('PUT /repos/{owner}/{repo}/contents/{path}', { owner: 'incrediblysecureinc', repo: 'incredibly-secure-Creastery', path: 'README.md', message: 'Escalated to Read-write Access', content: Buffer.from('Pwned!').toString('base64'), sha: '959c46eb0fbab9ab5b5bfb279ab6d70f720d1207' })
We see that, as discussed, @Creastery first hijacks the COMMENT_ID
step output variable using the aforementioned set-output workflow command. They then set the contents of this variable to the arbitrary Javascript they want to inject. Specifically, they closed out the initial assignment to const id =
with a value of 1
; and then injected a chunk of arbitrary Javascript code that reuses the already initialized github-script Action to push a commit to README.md
directly via the GitHub REST API. This works because the injected code is running in the context of a privileged Workflow, which has access to an active GitHub API token for the runtime of the Workflow.
Boom! Game over 🙂
This was the fastest and most straightforward way to solve the challenge. However, throughout the duration of the CTF, we witnessed many very creative approaches to solving this challenge.
Our absolute favorite of the bunch was submitted by @m-messiah who, even when committing through a templated Javascript injection from a comment body, refused to abandon branch etiquette. Instead, they used the injection to prepare a pull request and subsequently merged their PR into main.
@m-messiah, we salute you.
Best practices
The goal of this challenge was to introduce players to GitHub Workflow security considerations. One of the core messages is that GitHub Workflows should be treated as privileged code in almost all instances. This requires carefully considering potentially untrusted inputs, understanding Workflow trigger privileges, and following Actions best practices as much as possible.
Thank you everyone who took part:
From left to right: @0x77dev, @0xw01f, @4390c336, @95ych, @A-ZaWaYe, @A1ex-13, @ASVKVINAYAK, @AaronAtDuo, @Afvanjaffer, @Amarnathcdj, @Andrewgl22, @AnkilP, @AnomDevgun, @AnshumanFauzdar, @Arun-Sunny, @AshishKapoor, @Aurous, @AustinTice, @AydenB, @Barathrajsbr, @Bigtalljosh, @Bouldersky, @BrainIs404, @CamTosh, @ChampionBuffalo1, @ChoKyuWon, @ChrisGarrett23, @CodeWithAlvin, @Creastery, @DSchalla, @Dammmien, @DanielChuDC, @DevShahzad, @DeviaVir, @DolphinPhishing, @EGaddd, @Ex1st3nti4lCr1s1s, @FallenFoil, @FireFart, @GrantBirki, @GregTCLTK, @GregoireW, @Harshal0902, @Himanshukr000, @ISnackable, @IcarusCodes, @IdoHadar, @IshRaj, @JT117, @JamesPatrickGill, @Janberkb, @JaviMJ, @JettChenT, @KamalAres, @KethQv, @KevinSJ, @Kiddidddd, @LV, @LanglaitC, @LloydTao, @MR-A0101, @Moonlight-hello, @MrGithub2021, @NAVHITS, @NGUYENTRONGDAT123, @NetPenguins, @NiklasTerhorst, @NishantJoshi00, @Paradise123-bot-lang, @PiotrRut, @QuangNguyenVinh, @Raul6469, @Retr0-01, @Ri7Sh, @RiRa12621, @RitwikGopi, @RobDukarski, @Rocksus, @RonanLagan, @SAOMDVN, @Sijisu, @Skeeww, @SmoothMaverick, @Soham3-1415, @Sooraj-s-98, @SplittyDev, @TBgHg, @TarunShashank, @TheHackerCoding, @TheoMokos, @Toubster, @Tyrael, @V1NT4G3K0D3, @VaiTon, @Veershah26, @Wazbat, @XV1R, @Xeoth, @a-a-ron, @aagallag, @aashutoshrathi, @abbathaw, @adarsh1405, @adithyabsk, @adithyasunil26, @adnathanail, @adrianoapj, @ajh-sr, @ajithjunneti, @ajmilazzo, @alexrothenberg, @alphaX86, @alsebr, @anandrajaram21, @anandvalasseri, @aneeshdua, @antoinet, @apumax-1, @apuyou, @aqua95, @az9702w, @berviantoleo, @bgalek, @binarytrails, @bjansen, @blukid, @bramkragten, @cailloumajor, @callmekatootie, @chinggg, @chitoge, @chq-matteo, @chr0x6eos, @chriswood-cc, @chukkyiii, @cji, @cobianwuna, @crazymoose77756, @daetest, @danechitoaie, @danielpetrica, @darkpanda08, @dbeezt, @deadpixxl, @deniszh, @devhorizon53, @dgaponov, @dhrumilp15, @digitalwolframbler, @drwggm, @dwisiswant0, @ebubekirtrkr, @echo10032, @ejouv001, @elit8888, @emyei, @eric-winkler, @erjadi, @errietta, @evandrix, @evilpacket, @ewized, @eylamm, @f-barth, @f1ames, @fadhilthomas, @fcasal, @fegge, @foster, @franek, @frilox042, @frunkad, @g105b, @gaffneyd4, @georgettica, @ghctf2021, @ginkoid, @guyb1, @hadrianbs, @harshzalavadiya, @hax3xploit, @hellospacecorgi, @himanshu007-creator, @hmz99, @iam-abbas, @iamansoni, @iayushvarshney, @igorvoltaic, @imoisharma, @intrigus-lgtm, @isaidnocookies, @itspacchu, @iuryoliv, @ivan23kor, @jacklagare, @jakereps, @jamespeapen, @jarrodconnolly, @jasondantuma, @jasonericdavis, @jasongautama, @jatindhankhar, @jazibobs, @jennysharps, @jhnnsg, @jmatom, @jmthvt, @joaolaranjo, @johncorbin36, @jungsoo-shim, @just-hunter3, @juzzeth, @jvmvl, @kahla-sec, @karma9874, @khh-ini, @killshot13, @klassiker, @kmhalpin, @knowbibek, @konstruktoid, @kristoferanandita, @ksaid39, @kunalnagar, @lanavarrogs, @lapt0r, @leMedi, @leomoot, @listenToRipley, @lu-zen, @ludeeus, @lukeflima, @luxterful, @m-messiah, @maarlen, @maeserichar, @malik0011, @malwarebo, @manbonpan, @manishkumarr1017, @marinelli, @matthewmaclay, @maxam2017, @mazzaccaro, @mbiesiad, @mcharo, @med42, @meetmangukiya, @meroupatate, @mheap, @mnao305, @mosslilley, @mpslanker, @msimecek, @mxschmitt, @my-demo-github, @n-y-kim, @naortalmor1, @natusaspire, @naveen521kk, @nemesis545, @neverendingqs, @ngocdang499, @ngraef, @nickylogan, @nikitastupin, @notzheng, @ntjandra, @nurpabuccu, @obrientimothya, @okremer84, @olefriis, @oneturkmen, @orhanar, @paraschetal, @paulbreen-symphonytalent, @paulj, @pcy190, @pedro-javierf, @pedrodapp, @peterspbr, @phosfox, @pmccauley1994, @prashantkatiyar9777, @prathamesh-88, @pre7et, @prksu, @proxi, @purna1sai, @rahulsinghinfosec, @raichuAK, @ralacher, @ramshazar, @rcowsill, @redawl, @redtux, @rizalgowandy, @robisetiapermadi, @rockarts, @rojan-rijal, @romainmenke, @rose-m, @ryan-rozario, @s850042002, @sakshamgurbhele, @samgiz, @samuelrojasm, @saurav3199, @scalatar, @scottwn, @seano-vs, @sebader, @shamo0, @shiyandong, @shripadpaturkar, @skbasava, @smajchrz, @sms-system, @sodsec, @srkgupta, @ssupdoc, @stonejiajia, @sukolenvo, @supersat, @suresh, @swapshivam3, @syedalirazaidi, @teekenl, @tejasmorkar, @timoles, @tonghuikang, @trburgess, @tutasla, @tyage, @upitroma, @vedant3620, @veera83372, @vicktory22, @vinamramunot-tech, @vitallium, @vmwsree, @wei, @weitongttt, @willdurand, @xleepy, @xmunoz, @xue-yuan
We hope you had fun, and we hope to see you all again at a future challenge!
In the meantime, if you happen to be a Gopher, our Go CodeQL CTF is active until March 31!
Tags:
Written by
Related posts
Boost your CLI skills with GitHub Copilot
Want to know how to take your terminal skills to the next level? Whether you’re starting out, or looking for more advanced commands, GitHub Copilot can help us explain and suggest the commands we are looking for.
Beginner’s guide to GitHub: Setting up and securing your profile
As part of the GitHub for Beginners guide, learn how to improve the security of your profile and create a profile README. This will let you give your GitHub account a little more personality.
Beginner’s guide to GitHub: Merging a pull request
As part of the GitHub for Beginners guide, learn how to merge pull requests. This will enable you to resolve conflicts when they arise.