Web - The Cyber Jawara International 2024

This last october weekend i participated in cyber jawara international with team "swusjask fans club" we manage to secure 2nd position in the event.I personally contributed in solving 2 challs of web.

Example Box

Category
Points
Author

Web; whitebox

495

farisv

Code Analysis

Here is the main code that provided

from flask import Flask, abort, render_template, request, Response
from re import sub
from unidecode import unidecode
from urllib3.util import parse_url
import requests

app = Flask(__name__)

allowed_hostname = ["example.com"]
allowed_path = ["", "/"]
fallback = "http://example.com/"
cache = {}

def normalize(token):
    if token == None:
        token = ""
    return sub(r'\s+', '', unidecode(str(token)))

def filter_url(url):
    parsed_url = parse_url(url)
    scheme = normalize(parsed_url.scheme) # http
    host = normalize(parsed_url.host)
    path = normalize(parsed_url.path)
    filtered_url = url
    if not scheme.startswith('http'):
        filtered_url = fallback
    if not host in allowed_hostname:
        filtered_url = fallback
    if not path in allowed_path:
        filtered_url = fallback
    return normalize(filtered_url)

@app.route('/', methods=['GET', 'POST'])
def index():
    url = request.form.get('url', '')
    return render_template('index.html', url=url)

@app.route('/fetch_url')
def fetch_url():
    url = request.args.get('url')
    filtered_url = filter_url(url)
    print("request from: ", request.remote_addr)
    # print("cache now: ")
    try:
        if filtered_url in cache:
            response = cache[filtered_url]
        else:
            response = requests.get(filtered_url)
            cache[filtered_url] = response
        return Response(response.content,
                        status=response.status_code,
                        content_type=response.headers.get('Content-Type'))
    except requests.exceptions.RequestException as e:
        return f"Error fetching the URL: {e}", 500

@app.route('/flag')
def flag():
    if request.remote_addr != '127.0.0.1':
        abort(403)
    with open('/flag.txt', 'r') as flag:
        return flag.read()

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=20002)
    

Reviewing the code we know that, this must be related to ssrf but there is some url parsing filter that we should bypass. Although the code looks very simple but i found that its really tricky to bypass.

Our objective is to access path flag with 127.0.0.1 remote addres http://127.0.01/flag

flag():
    if request.remote_addr != '127.0.0.1':
        abort(403)
    with open('/flag.txt', 'r') as flag:
        return flag.read()

Reviewing the code the normalize function is to check if there is whitespace or any unicode in the url.

filter_url is check for the whitelist.

def filter_url(url):
    parsed_url = parse_url(url)
    scheme = normalize(parsed_url.scheme) # http
    host = normalize(parsed_url.host)
    path = normalize(parsed_url.path)
    filtered_url = url
    if not scheme.startswith('http'):
        filtered_url = fallback
    if not host in allowed_hostname:
        filtered_url = fallback
    if not path in allowed_path:
        filtered_url = fallback
    return normalize(filtered_url)

the challenge is we need to bypass parse_url and fallback overwrite, to perform this there is a good reads that that i found for bypassing the parse_url() in python.

https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf

Yepp we can use @ to bypass the parse_url,

Exploitation

Because there are some whitelist so we need to use it to perform SSRF

allowed_hostname = ["example.com"]
allowed_path = ["", "/"]
fallback = "http://example.com/"
cache = {}

I perform some test code to debug the website and see how the parsed works

We can see we have finally bypass the parse_url and fallback. but the request python still accepting our request as example.com

the problem is if we access the path flag manually like this http://127.0.0.1/flag@example.com the parse will works again and our input will be only example.com.

So we need to use unicode because normalize function and we are using ? for the reuqest to not accessing path @example.com.

So here is the full payload:

http://127.0.0.1:20002/ⓕⓛⓐⓖ?@example.com

Flag: CJ{enough_with_example.com_here_is_your_nice_Phl4g!}

Java Box

Category
Points
Author

Java Box; blackbox

499

farisv

In this challenge our team work together to solve the challenge. Thanks to @daffainfo who find the initial foothold of the challenge we can continue the challenge and manage to solve 1 hour before the CTF ends.

Blackbox

We are given a service and there is no code provided so it should be blackbox challenge. There is only register and login to the dahsboard and nothing special with other feature of the service.

Our team found that we can see java stack trace in assets path. After that we started by doing some enumeration there.

If we're accessing assets the error given is java.lang.StringIndexOutOfBoundsException accessing But if we are accessing something like this the error given is different, it look like failed to get some resouce in the server we consider that is trying to access file in the server.

Exploitation

After some time we find a good article expalaing about some new release CVEs

https://attackerkb.com/assessments/25397f72-670e-4ef4-a19b-2a3a55120d18?referrer=profile

But even is simmiliar we didnt find the real objective and still struggling, then several time my team friend got something like this

After that From here, i figue out why not trying to access the path traversal like in java folder as usual im asking chat gpt for that and yep we found 200 status. But we didnt manage to get Main Controller at first.

We forgot that is being compiled so its not .java but .class here is the code we find

com.cyberjawara.chall.web.javabox.controller.MainController

package com.cyberjawara.chall.web.javabox.controller;

import com.cyberjawara.chall.web.javabox.util.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class MainController {
   @GetMapping({"/"})
   public String index() {
      return "index";
   }

   @GetMapping({"/register"})
   public String registerPage() {
      return "register";
   }

   @PostMapping({"/register"})
   public String register(@RequestParam String username, @RequestParam String password, HttpServletResponse response) {
      if (username != null && password != null && username.length() > 3 && password.length() > 3) {
         String jwt = JwtUtil.generateToken(username, false);
         Cookie cookie = new Cookie("jwt", jwt);
         cookie.setHttpOnly(true);
         cookie.setPath("/");
         response.addCookie(cookie);
         return "redirect:/dashboard";
      } else {
         return "redirect:/register";
      }
   }

   @GetMapping({"/dashboard"})
   public String dashboard(@CookieValue(value = "jwt",defaultValue = "") String jwt, Model model) {
      try {
         Claims claims = JwtUtil.validateToken(jwt);
         String username = (String)claims.get("username", String.class);
         Boolean isAdmin = (Boolean)claims.get("isAdmin", Boolean.class);
         if (isAdmin) {
            String filePath = "/flag.txt";

            try {
               BufferedReader br = new BufferedReader(new FileReader(filePath));

               try {
                  String content = br.readLine();
                  model.addAttribute("flag", content);
               } catch (Throwable var11) {
                  try {
                     br.close();
                  } catch (Throwable var10) {
                     var11.addSuppressed(var10);
                  }

                  throw var11;
               }

               br.close();
            } catch (IOException var12) {
            }
         }

         model.addAttribute("username", username);
         model.addAttribute("isAdmin", isAdmin);
         return "dashboard";
      } catch (Exception var13) {
         return "redirect:/register";
      }
   }

   @GetMapping({"/logout"})
   public String logout(HttpServletResponse response) {
      Cookie jwtCookie = new Cookie("jwt", "");
      jwtCookie.setMaxAge(0);
      jwtCookie.setPath("/");
      jwtCookie.setHttpOnly(true);
      response.addCookie(jwtCookie);
      return "redirect:/";
   }

   @GetMapping({"/assets/**"})
   public ResponseEntity<byte[]> getAssetFile(HttpServletRequest request) throws Exception {
      String requestURI = request.getRequestURI();
      String resourcePath = "/assets/" + requestURI.substring("/assets/".length());

      try {
         InputStream inputStream = this.getClass().getResourceAsStream(resourcePath);

         ResponseEntity var8;
         try {
            if (inputStream == null || !hasExtension(resourcePath)) {
               throw new Exception("getResourceAsStream failed");
            }

            byte[] fileContent = inputStream.readAllBytes();
            String mimeType = Files.probeContentType(Path.of(resourcePath, new String[0]));
            if (mimeType == null) {
               if (resourcePath.endsWith(".css")) {
                  mimeType = "text/css";
               } else {
                  mimeType = "text/plain";
               }
            }

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.parseMediaType(mimeType));
            var8 = new ResponseEntity(fileContent, headers, HttpStatus.OK);
         } catch (Throwable var10) {
            if (inputStream != null) {
               try {
                  inputStream.close();
               } catch (Throwable var9) {
                  var10.addSuppressed(var9);
               }
            }

            throw var10;
         }

         if (inputStream != null) {
            inputStream.close();
         }

         return var8;
      } catch (IOException var11) {
         return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body((Object)null);
      }
   }

   public static boolean hasExtension(String filename) {
      if (filename != null && !filename.isEmpty()) {
         if (filename.length() < 3) {
            return false;
         } else {
            int dotIndex = filename.lastIndexOf(46);
            return dotIndex > 0 && dotIndex < filename.length() - 2;
         }
      } else {
         return false;
      }
   }
}

Analyzing the source code is obvious we need to access admin to get the flag so we need to construct our jwt but we need the jwt secret key. We found it in:

com.cyberjawara.chall.web.javabox.util.JwtUtil.class

the key we found: c31bcd4ffcff8e971a6ad6ddcbdc613a1246f4223c00fa37404b501ad749257c

From here we struggle a little bit changing the jwt.io but our tem found that we need to use token.dev for the jwt thanks to @Lyyn

Then we change the jwt for the win :)

Flag: CJ{black_box_web_testing_is_not_that_bad_and_too_guessy_right?}

Last updated