Keywords
- Github Action
- octokit library in Nodejs
Given info
- the github URL and sdk_server_source.js were given
const dotenv = require("dotenv");
const express = require("express");
const octokit = require("octokit");
const url = require("url");
const app = express();
const SHA256 = require("crypto-js/sha256");
const port = 1337;
dotenv.config();
const TOKEN = process.env.TOKEN;
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var tokens = {};
var identities = {};
function randomStr(len) {
let res = "";
for (let i = len; i > 0; i--) {
res += chars[Math.floor(Math.random() * chars.length)];
}
return res;
}
app.get("/", (req, res) => {
res.send("Empty route for / for k8s");
});
app.get("/gettoken", (req, res) => {
let tokenCode = randomStr(32);
let tokenObj = {
tokenCode: tokenCode,
identityCode: undefined,
timeCreated: Math.floor(Date.now() / 1000),
status: "UNVALIDATED"
};
tokens[tokenCode] = tokenObj;
console.log("/gettoken: " + tokenCode);
res.send(tokenCode);
});
app.get("/getiden", (req, res) => {
let q = url.parse(req.url, true).query;
if (!q.sdktok) {
res.send("missing sdktok");
return;
} else if (!q.repo) {
res.send("missing repo");
return;
} else if (!q.runid) {
res.send("missing runid");
return;
}
let tokenCode = q.sdktok;
let tokenObj = tokens[tokenCode];
if (tokenObj == undefined) {
res.send("token doesn't exist!");
return;
}
if (!hasTokenNotExpired(tokenObj)) {
res.send("token expired!");
return;
}
let ownerName = q.repo.toString().split("/")[0];
let repoName = q.repo.toString().split("/")[1];
let identityCode = randomStr(32);
let identityObj = {
identityCode: identityCode,
tokenCode: q.sdktok.toString(),
owner: ownerName,
repo: repoName,
runId: q.runid.toString(),
timeCreated: Math.floor(Date.now() / 1000)
};
identities[identityCode] = identityObj;
tokens[tokenCode].identity = identityCode;
console.log("/getiden: " + identityCode);
res.send(identityCode);
});
app.get("/checkiden", async (req, res) => {
let q = url.parse(req.url, true).query;
if (!q.sdktok) {
res.send("missing sdktok");
return;
}
let tokenCode = q.sdktok;
let tokenObj = tokens[tokenCode];
if (tokenObj == undefined) {
res.send("token doesn't exist!");
return;
}
if (!hasTokenNotExpired(tokenObj)) {
res.send("token expired!");
return;
}
let identityObj = identities[tokenObj.identity];
var repoIdentity;
try {
repoIdentity = await getRepoIdentity(identityObj.owner, identityObj.repo, identityObj.runId);
} catch {
repoIdentity = undefined;
}
if (repoIdentity == undefined) {
res.send("repo identity failed!");
return;
}
if (repoIdentity != identityObj.identityCode) {
res.send(`repo identity failed! found ${repoIdentity} but expected ${identityObj.identityCode}`);
return;
}
tokenObj.status = "VALIDATED";
console.log("/checkiden: " + "OK");
res.send("OK");
});
app.get("/getsdk", (req, res) => {
let q = url.parse(req.url, true).query;
if (!q.sdktok) {
res.send("missing sdktok");
return;
}
let tokenCode = q.sdktok;
let tokenObj = tokens[tokenCode];
if (tokenObj == undefined) {
res.send("token doesn't exist!");
return;
}
if (!hasTokenNotExpired(tokenObj)) {
res.send("token expired!");
return;
}
if (tokenObj.status == "VALIDATED") {
tokenObj.status = "EXPIRED";
res.sendFile("coolsdk.tar.gz", {root: __dirname});
} else {
res.send("no u");
}
});
app.listen(port, () => {
console.log(`My cool sdk server running on ${port}`);
});
function hasTokenNotExpired(tokenObj) {
if (tokenObj.status == "EXPIRED" || (Date.now() / 1000) - tokenObj.timeCreated > 120) {
tokenObj.status = "EXPIRED";
return false;
}
return true;
}
async function getRepoIdentity(owner, repo, runId) {
const octo = new octokit.Octokit({
auth: TOKEN
});
let runJobInfo = await octo.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs", {
owner: owner,
repo: repo,
run_id: runId
});
let jobLogInfo = await octo.request("GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs", {
owner: owner,
repo: repo,
job_id: runJobInfo.data.jobs[0].id
});
let logLines = jobLogInfo.data.split(/\r?\n/);
let startPos = 0;
for (let i = 0; i < logLines.length; i++) {
if (logLines[i].includes("Job is about to start running on the hosted runner")) {
startPos = i + 1;
break;
}
}
let idenStr = undefined;
const target = "ServerID-";
for (let i = startPos; i < logLines.length; i++) {
if (logLines[i].includes(target) && !logLines[i].includes("echo")) {
let idx = logLines[i].indexOf(target);
let len = target.length;
idenStr = logLines[i].substring(idx + len);
break;
}
if (logLines[i].includes("##[debug]")) {
return undefined;
}
}
if (idenStr == undefined) {
return undefined;
}
let runInfo = await octo.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
owner: owner,
repo: repo,
run_id: runId
});
let runHeadSha = runInfo.data.head_sha;
let runFilePath = runInfo.data.path;
let fileInfo = await octo.request("GET /repos/{owner}/{repo}/contents/{path}{?ref}", {
owner: owner,
repo: repo,
path: runFilePath,
ref: runHeadSha
});
let workflowContents = Buffer.from(fileInfo.data.content, "base64");
let workflowHash = SHA256(workflowContents.toString()).toString();
const MATCH_HASH = "8951a3d29206cf24600379fba7efeb7d8fc9181353a958f710a32eb786ae8654";
if (workflowHash != MATCH_HASH) {
console.log(`expected ${MATCH_HASH}, found ${workflowHash} hash for workflow!`);
return undefined;
}
console.log(idenStr);
return idenStr;
}
app.get("/getsdk", (req, res) => {
let q = url.parse(req.url, true).query;
if (!q.sdktok) {
res.send("missing sdktok");
return;
}
let tokenCode = q.sdktok;
let tokenObj = tokens[tokenCode];
if (tokenObj == undefined) {
res.send("token doesn't exist!");
return;
}
if (!hasTokenNotExpired(tokenObj)) {
res.send("token expired!");
return;
}
if (tokenObj.status == "VALIDATED") {
tokenObj.status = "EXPIRED";
res.sendFile("coolsdk.tar.gz", {root: __dirname});
} else {
res.send("no u");
}
});
- The target is
coolsdk.tar.gz
file, we should passing those if() functions
flow
- set sdktok
- set validated token
- set coolsdk.tar.gz file
get coolsdk.tar.gz
- using /gettoken, we can get tokenCode data
root@DESKTOP-CK998RL:~
R3mQdQHsasd6D9Izzv1z4YcAsPRDdhT9
root@DESKTOP-CK998RL:~
bQlci9Cw5vX9v3x0Zv8qzlQxPZmJvOEr
root@DESKTOP-CK998RL:~
m9QIRjG2qfDUDJRWBmSaxDYQM3PGgRdv
root@DESKTOP-CK998RL:~
DiZJlPX1PDEEHiDcKlXFuFDsoJHFY5pt
root@DESKTOP-CK998RL:~
OJ2CoIoFNONsEncutlNsmtAtgNHd7xUX
- using /getiden, we can get the identifyCode data
- we can see that server will execute getRepoIdentity() function in /checkiden
async function getRepoIdentity(owner, repo, runId) {
const octo = new octokit.Octokit({
auth: TOKEN
});
let runJobInfo = await octo.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs", {
owner: owner,
repo: repo,
run_id: runId
});
let jobLogInfo = await octo.request("GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs", {
owner: owner,
repo: repo,
job_id: runJobInfo.data.jobs[0].id
});
let logLines = jobLogInfo.data.split(/\r?\n/);
let startPos = 0;
for (let i = 0; i < logLines.length; i++) {
if (logLines[i].includes("Job is about to start running on the hosted runner")) {
startPos = i + 1;
break;
}
}
let idenStr = undefined;
const target = "ServerID-";
for (let i = startPos; i < logLines.length; i++) {
if (logLines[i].includes(target) && !logLines[i].includes("echo")) {
let idx = logLines[i].indexOf(target);
let len = target.length;
idenStr = logLines[i].substring(idx + len);
break;
}
if (logLines[i].includes("##[debug]")) {
return undefined;
}
}
if (idenStr == undefined) {
return undefined;
}
let runInfo = await octo.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
owner: owner,
repo: repo,
run_id: runId
});
let runHeadSha = runInfo.data.head_sha;
let runFilePath = runInfo.data.path;
let fileInfo = await octo.request("GET /repos/{owner}/{repo}/contents/{path}{?ref}", {
owner: owner,
repo: repo,
path: runFilePath,
ref: runHeadSha
});
let workflowContents = Buffer.from(fileInfo.data.content, "base64");
let workflowHash = SHA256(workflowContents.toString()).toString();
const MATCH_HASH = "8951a3d29206cf24600379fba7efeb7d8fc9181353a958f710a32eb786ae8654";
if (workflowHash != MATCH_HASH) {
console.log(`expected ${MATCH_HASH}, found ${workflowHash} hash for workflow!`);
return undefined;
}
console.log(idenStr);
return idenStr;
}
- server will octo.request() to get information from github
...
let runInfo = await octo.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
owner: owner,
repo: repo,
run_id: runId
});
...
- We need own github action run_id
- The given github repo is https://github.com/IrisSec/MyCoolEncryptor/ but, we can not do github Actions in IrisSec's Repo
- Thus, fork the MyCoolEncryptor and make our own Action run_id
- Now we can get run_id : 4022336164
- And make identityCode and checkit
root@DESKTOP-CK998RL:~
4GQcYeSfSsnPIZ7pnYLtXqvf78QsvkwT
root@DESKTOP-CK998RL:~
root@DESKTOP-CK998RL:~
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 21 100 21 0 0 13 0 0:00:01 0:00:01 --:--:-- 13
repo identity failed!
root@DESKTOP-CK998RL:~
- check the code to find
repo identity failed!
- We can see that repoIdentity should not be undefined
if (repoIdentity == undefined) {
res.send("repo identity failed!");
return;
}
- The repoIdentity value is set by
var repoIdentity;
try {
repoIdentity = await getRepoIdentity(identityObj.owner, identityObj.repo, identityObj.runId);
} catch {
repoIdentity = undefined;
}
- If occured any error, the repoIdentity will be undefined, we set all information octo.request() neeeded. owner, repo and run_id
- Thus, octo.request() don't have any problem
- But, they have "scan for server id string" logic
let idenStr = undefined;
const target = "ServerID-";
for (let i = startPos; i < logLines.length; i++) {
if (logLines[i].includes(target) && !logLines[i].includes("echo")) {
let idx = logLines[i].indexOf(target);
let len = target.length;
idenStr = logLines[i].substring(idx + len);
break;
}
if (logLines[i].includes("##[debug]")) {
return undefined;
}
}
if (idenStr == undefined) {
return undefined;
}
- The target should be start with
ServerID-
, then they set the idenStr value and keep go to the next codes
- I changed my repo name from 'thisisyoobi/MyCoolEncryptor' to 'thisisyoobi/ServerID-yoobi', then we can see this failed comment
root@DESKTOP-CK998RL:~
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 79 100 79 0 0 29 0 0:00:02 0:00:02 --:--:-- 29
repo identity failed! found yoobi but expected d7bAo9Ys3LfYENdpOSzhIejYLeggaUPn
- the expected value is d7bAo9Ys3LfYENdpOSzhIejYLeggaUPn, and that is the identityCode value
- Thus, change the repo name to ServerID-the_identityCode_give will make us to go to the next codes
root@DESKTOP-CK998RL:~
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2 0 2 0 0 0 0 --:--:-- 0:00:03 --:--:-- 0
OK
- The server send me OK, we successfully passing all hurdles
- Then, execute /getsdk to get coolsdk.tar.gz file
root@DESKTOP-CK998RL:~
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1327 100 1327 0 0 215k 0 --:--:-- --:--:-- --:--:-- 215k
root@DESKTOP-CK998RL:~
coolsdk.tar.gz
root@DESKTOP-CK998RL:~
coolsdk/
coolsdk/cool.c
coolsdk/cool.h
coolsdk/readme.txt
root@DESKTOP-CK998RL:~
build coolsdk & get FLAG
- now we got coolsdk files cool.c, cool.h
- we have readme.txt
the cool sdk
------------
encrypt all your data with this cool sdk
(c) cool guy 2023
test decryption with:
ECDDD6B8B742A2015E9DBE50C20BE8094BF3084033325A0DCFA81896CDF5826C7EA68F320FC75DA3F776
69420
- go to build using the makefile given
all: coolencrypt
coolencrypt: coolsdk/cool.c program.c
$(CC) coolsdk/cool.c program.c -o coolencrypt
root@DESKTOP-CK998RL:~
cc coolsdk/cool.c program.c -o coolencrypt
root@DESKTOP-CK998RL:~
MyCoolEncryptor coolencrypt coolsdk coolsdk.tar.gz makefile program.c readme.md
root@DESKTOP-CK998RL:~
- execute coolencrypt to decrypt the FLAG
root@DESKTOP-CK998RL:~
Welcome to my cool encryptor program!
Encrypt (0), decrypt (1), or exit (2)?
1
Type the message to decrypt: ECDDD6B8B742A2015E9DBE50C20BE8094BF3084033325A0DCFA81896CDF5826C7EA68F320FC75DA3F776
Type numeric key code to decrypt with: 69420
irisCTF{my_sdk_wasnt_so_private_after_all}
Encrypt (0), decrypt (1), or exit (2)?
2
Goodbye!root@DESKTOP-CK998RL:~