GitHub Capture the Flag results

Image of Bas Alberts

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:

Avatar of @0x77dev Avatar of @0xw01f Avatar of @4390c336 Avatar of @95ych Avatar of @A-ZaWaYe Avatar of @A1ex-13 Avatar of @ASVKVINAYAK Avatar of @AaronAtDuo Avatar of @Afvanjaffer Avatar of @Amarnathcdj Avatar of @Andrewgl22 Avatar of @AnkilP Avatar of @AnomDevgun Avatar of @AnshumanFauzdar Avatar of @Arun-Sunny Avatar of @AshishKapoor Avatar of @Aurous Avatar of @AustinTice Avatar of @AydenB Avatar of @Barathrajsbr Avatar of @Bigtalljosh Avatar of @Bouldersky Avatar of @BrainIs404 Avatar of @CamTosh Avatar of @ChampionBuffalo1 Avatar of @ChoKyuWon Avatar of @ChrisGarrett23 Avatar of @CodeWithAlvin Avatar of @Creastery Avatar of @DSchalla Avatar of @Dammmien Avatar of @DanielChuDC Avatar of @DevShahzad Avatar of @DeviaVir Avatar of @DolphinPhishing Avatar of @EGaddd Avatar of @Ex1st3nti4lCr1s1s Avatar of @FallenFoil Avatar of @FireFart Avatar of @GrantBirki Avatar of @GregTCLTK Avatar of @GregoireW Avatar of @Harshal0902 Avatar of @Himanshukr000 Avatar of @ISnackable Avatar of @IcarusCodes Avatar of @IdoHadar Avatar of @IshRaj Avatar of @JT117 Avatar of @JamesPatrickGill Avatar of @Janberkb Avatar of @JaviMJ Avatar of @JettChenT Avatar of @KamalAres Avatar of @KethQv Avatar of @KevinSJ Avatar of @Kiddidddd Avatar of @LV Avatar of @LanglaitC Avatar of @LloydTao Avatar of @MR-A0101 Avatar of @Moonlight-hello Avatar of @MrGithub2021 Avatar of @NAVHITS Avatar of @NGUYENTRONGDAT123 Avatar of @NetPenguins Avatar of @NiklasTerhorst Avatar of @NishantJoshi00 Avatar of @Paradise123-bot-lang Avatar of @PiotrRut Avatar of @QuangNguyenVinh Avatar of @Raul6469 Avatar of @Retr0-01 Avatar of @Ri7Sh Avatar of @RiRa12621 Avatar of @RitwikGopi Avatar of @RobDukarski Avatar of @Rocksus Avatar of @RonanLagan Avatar of @SAOMDVN Avatar of @Sijisu Avatar of @Skeeww Avatar of @SmoothMaverick Avatar of @Soham3-1415 Avatar of @Sooraj-s-98 Avatar of @SplittyDev Avatar of @TBgHg Avatar of @TarunShashank Avatar of @TheHackerCoding Avatar of @TheoMokos Avatar of @Toubster Avatar of @Tyrael Avatar of @V1NT4G3K0D3 Avatar of @VaiTon Avatar of @Veershah26 Avatar of @Wazbat Avatar of @XV1R Avatar of @Xeoth Avatar of @a-a-ron Avatar of @aagallag Avatar of @aashutoshrathi Avatar of @abbathaw Avatar of @adarsh1405 Avatar of @adithyabsk Avatar of @adithyasunil26 Avatar of @adnathanail Avatar of @adrianoapj Avatar of @ajh-sr Avatar of @ajithjunneti Avatar of @ajmilazzo Avatar of @alexrothenberg Avatar of @alphaX86 Avatar of @alsebr Avatar of @anandrajaram21 Avatar of @anandvalasseri Avatar of @aneeshdua Avatar of @antoinet Avatar of @apumax-1 Avatar of @apuyou Avatar of @aqua95 Avatar of @az9702w Avatar of @berviantoleo Avatar of @bgalek Avatar of @binarytrails Avatar of @bjansen Avatar of @blukid Avatar of @bramkragten Avatar of @cailloumajor Avatar of @callmekatootie Avatar of @chinggg Avatar of @chitoge Avatar of @chq-matteo Avatar of @chr0x6eos Avatar of @chriswood-cc Avatar of @chukkyiii Avatar of @cji Avatar of @cobianwuna Avatar of @crazymoose77756 Avatar of @daetest Avatar of @danechitoaie Avatar of @danielpetrica Avatar of @darkpanda08 Avatar of @dbeezt Avatar of @deadpixxl Avatar of @deniszh Avatar of @devhorizon53 Avatar of @dgaponov Avatar of @dhrumilp15 Avatar of @digitalwolframbler Avatar of @drwggm Avatar of @dwisiswant0 Avatar of @ebubekirtrkr Avatar of @echo10032 Avatar of @ejouv001 Avatar of @elit8888 Avatar of @emyei Avatar of @eric-winkler Avatar of @erjadi Avatar of @errietta Avatar of @evandrix Avatar of @evilpacket Avatar of @ewized Avatar of @eylamm Avatar of @f-barth Avatar of @f1ames Avatar of @fadhilthomas Avatar of @fcasal Avatar of @fegge Avatar of @foster Avatar of @franek Avatar of @frilox042 Avatar of @frunkad Avatar of @g105b Avatar of @gaffneyd4 Avatar of @georgettica Avatar of @ghctf2021 Avatar of @ginkoid Avatar of @guyb1 Avatar of @hadrianbs Avatar of @harshzalavadiya Avatar of @hax3xploit Avatar of @hellospacecorgi Avatar of @himanshu007-creator Avatar of @hmz99 Avatar of @iam-abbas Avatar of @iamansoni Avatar of @iayushvarshney Avatar of @igorvoltaic Avatar of @imoisharma Avatar of @intrigus-lgtm Avatar of @isaidnocookies Avatar of @itspacchu Avatar of @iuryoliv Avatar of @ivan23kor Avatar of @jacklagare Avatar of @jakereps Avatar of @jamespeapen Avatar of @jarrodconnolly Avatar of @jasondantuma Avatar of @jasonericdavis Avatar of @jasongautama Avatar of @jatindhankhar Avatar of @jazibobs Avatar of @jennysharps Avatar of @jhnnsg Avatar of @jmatom Avatar of @jmthvt Avatar of @joaolaranjo Avatar of @johncorbin36 Avatar of @jungsoo-shim Avatar of @just-hunter3 Avatar of @juzzeth Avatar of @jvmvl Avatar of @kahla-sec Avatar of @karma9874 Avatar of @khh-ini Avatar of @killshot13 Avatar of @klassiker Avatar of @kmhalpin Avatar of @knowbibek Avatar of @konstruktoid Avatar of @kristoferanandita Avatar of @ksaid39 Avatar of @kunalnagar Avatar of @lanavarrogs Avatar of @lapt0r Avatar of @leMedi Avatar of @leomoot Avatar of @listenToRipley Avatar of @lu-zen Avatar of @ludeeus Avatar of @lukeflima Avatar of @luxterful Avatar of @m-messiah Avatar of @maarlen Avatar of @maeserichar Avatar of @malik0011 Avatar of @malwarebo Avatar of @manbonpan Avatar of @manishkumarr1017 Avatar of @marinelli Avatar of @matthewmaclay Avatar of @maxam2017 Avatar of @mazzaccaro Avatar of @mbiesiad Avatar of @mcharo Avatar of @med42 Avatar of @meetmangukiya Avatar of @meroupatate Avatar of @mheap Avatar of @mnao305 Avatar of @mosslilley Avatar of @mpslanker Avatar of @msimecek Avatar of @mxschmitt Avatar of @my-demo-github Avatar of @n-y-kim Avatar of @naortalmor1 Avatar of @natusaspire Avatar of @naveen521kk Avatar of @nemesis545 Avatar of @neverendingqs Avatar of @ngocdang499 Avatar of @ngraef Avatar of @nickylogan Avatar of @nikitastupin Avatar of @notzheng Avatar of @ntjandra Avatar of @nurpabuccu Avatar of @obrientimothya Avatar of @okremer84 Avatar of @olefriis Avatar of @oneturkmen Avatar of @orhanar Avatar of @paraschetal Avatar of @paulbreen-symphonytalent Avatar of @paulj Avatar of @pcy190 Avatar of @pedro-javierf Avatar of @pedrodapp Avatar of @peterspbr Avatar of @phosfox Avatar of @pmccauley1994 Avatar of @prashantkatiyar9777 Avatar of @prathamesh-88 Avatar of @pre7et Avatar of @prksu Avatar of @proxi Avatar of @purna1sai Avatar of @rahulsinghinfosec Avatar of @raichuAK Avatar of @ralacher Avatar of @ramshazar Avatar of @rcowsill Avatar of @redawl Avatar of @redtux Avatar of @rizalgowandy Avatar of @robisetiapermadi Avatar of @rockarts Avatar of @rojan-rijal Avatar of @romainmenke Avatar of @rose-m Avatar of @ryan-rozario Avatar of @s850042002 Avatar of @sakshamgurbhele Avatar of @samgiz Avatar of @samuelrojasm Avatar of @saurav3199 Avatar of @scalatar Avatar of @scottwn Avatar of @seano-vs Avatar of @sebader Avatar of @shamo0 Avatar of @shiyandong Avatar of @shripadpaturkar Avatar of @skbasava Avatar of @smajchrz Avatar of @sms-system Avatar of @sodsec Avatar of @srkgupta Avatar of @ssupdoc Avatar of @stonejiajia Avatar of @sukolenvo Avatar of @supersat Avatar of @suresh Avatar of @swapshivam3 Avatar of @syedalirazaidi Avatar of @teekenl Avatar of @tejasmorkar Avatar of @timoles Avatar of @tonghuikang Avatar of @trburgess Avatar of @tutasla Avatar of @tyage Avatar of @upitroma Avatar of @vedant3620 Avatar of @veera83372 Avatar of @vicktory22 Avatar of @vinamramunot-tech Avatar of @vitallium Avatar of @vmwsree Avatar of @wei Avatar of @weitongttt Avatar of @willdurand Avatar of @xleepy Avatar of @xmunoz Avatar of @xue-yuan

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!