Web Backend


Checking the backend of the target web application after performing a basic system enumeration

PS C:\Web> ls
 
    Directory: C:\Web
 
 
Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
d-----        2/25/2024   4:53 PM                DB Backups                                                            
d-----        2/12/2024   4:54 PM                nginx-1.24.0                                                          
d-----       10/27/2024   1:36 AM                University

The C:\Web directory is the dedicated directory for the target web application There are 3 sub-directories;

  • C:\Web\DB Backups for backup operation
  • C:\Web\nginx-1.24.0 for the web server & proxy configuration
  • C:\Web\University is the web application itself

nginx


PS C:\Web> cd nginx-1.24.0 ; ls
 
    Directory: C:\Web\nginx-1.24.0
 
 
Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
d-----        2/12/2024   5:47 PM                conf                                                                  
d-----        2/12/2024   3:46 PM                contrib                                                               
d-----        2/12/2024   3:46 PM                docs                                                                  
d-----        2/12/2024   3:46 PM                html                                                                  
d-----        2/17/2024   3:06 AM                logs                                                                  
d-----        2/12/2024   4:45 PM                temp                                                                  
-a----        4/11/2023   8:29 AM        3811328 nginx.exe                                                             
-a----         3/3/2024   4:10 AM         111067 off                                                                   
-a----        2/15/2024  12:36 AM             46 start.bat                                                             

Checking the nginx directory. There is the configuration directory

PS C:\Web\nginx-1.24.0> cd conf ; ls
 
    Directory: C:\Web\nginx-1.24.0\conf
 
 
Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
-a----        4/11/2023   8:31 AM           1103 fastcgi.conf                                                          
-a----        4/11/2023   8:31 AM           1032 fastcgi_params                                                        
-a----        4/11/2023   8:31 AM           2946 koi-utf                                                               
-a----        4/11/2023   8:31 AM           2326 koi-win                                                               
-a----        4/11/2023   8:31 AM           5448 mime.types                                                            
-a----       10/18/2024   2:53 PM           1501 nginx.conf                                                            
-a----        4/11/2023   8:31 AM           2773 nginx.conf.bak                                                        
-a----        4/11/2023   8:31 AM            653 scgi_params                                                           
-a----        4/11/2023   8:31 AM            681 uwsgi_params                                                          
-a----        4/11/2023   8:31 AM           3736 win-utf                                                               

nginx.conf

Nginx Configuration


PS C:\Web\nginx-1.24.0\conf> cat nginx.conf
 
#user  nobody;
worker_processes  1;
error_log  off;
#pid        logs/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    access_log  off;
    log_not_found off;
    sendfile        on;
    keepalive_timeout  65;
    server_names_hash_bucket_size  64;
    server {
    listen 0.0.0.0:80;  # Listen on HTTP port 80
    server_name "^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$";
 
    location / {
        return 301 http://university.htb$request_uri;
      }
    }
    server {
        listen 0.0.0.0:80;
        server_name  university.htb;
        client_max_body_size 3M;
        location = /favicon.ico {
                alias "C:\Web\University\static\assets\images\favicon.ico";
                access_log off;
                log_not_found off;
        }
        location /static/ {
                root "C:\Web\University";
        }
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
        location / {
                proxy_set_header Host $http_host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_pass http://127.0.0.1:8000;
        }
    }
}

proxy_pass http://127.0.0.1:8000;

Web Application


PS C:\Web> cd University ; ls
 
 
    Directory: C:\Web\University
 
 
Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
d-----        2/15/2024   8:13 AM                CA                                                                    
d-----       10/27/2024  12:32 AM                static                                                                
d-----       10/15/2024  11:42 AM                University                                                            
-a----       10/27/2024   1:36 AM           5419 CW8nkQ.html                                                           
-a----       10/27/2024   1:36 AM              0 CW8nkQ.pdf                                                            
-a----       10/27/2024   1:36 AM         245760 db.sqlite3                                                            
-a----        12/3/2023   4:28 AM            666 manage.py                                                             
-a----        2/15/2024  12:51 AM            133 start-server.bat                                                      

Web application directory

start-server.bat


PS C:\Web\University> cat start-server.bat
cat start-server.bat
@ECHO OFF
cd C:\Web\University
"C:\Program Files\Python310\python.exe" -u .\manage.py runserver --noreload --no-color --skip-checks

The batch file is likely run via a scheduled task

manage.py


PS C:\Web\University> cat manage.py
cat manage.py
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
 
 
def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'University.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)
 
 
if __name__ == '__main__':
    main()

The manage.py file for management It loads University.settings

db.sqlite3


PS C:\Web\University> copy db.sqlite3 .\static\db.sqlite3
 
┌──(kali㉿kali)-[~/archive/htb/labs/university]
└─$ curl -s http://university.htb/static/db.sqlite3 -o ./db.sqlite3

I will also transfer the DB file

┌──(kali㉿kali)-[~/archive/htb/labs/university]
└─$ file db.sqlite3 
db.sqlite3: SQLite 3.x database, last written using SQLite version 3037002, file counter 596, database pages 60, 1st free page 60, free pages 3, cookie 0x5b, schema 4, UTF-8, version-valid-for 596
 
┌──(kali㉿kali)-[~/archive/htb/labs/university]
└─$ open db.sqlite3

Opening it up

There are a total of 16 tables

University_customuser Table


The University_customuser table contains user data I will grab all the password hashes. The hash format appears to be pbkdf2 from werkzeug.security . This requires reformatting in order to crack them.

University_professor Table


The University_professor table shows PGP public key endpoints for professor users

django_migrations Table


The django_migrations table appears to shows migration logs

CA


PS C:\Web\University> cd CA ; ls
 
 
    Directory: C:\Web\University\CA
 
 
Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
-a----        2/15/2024   5:51 AM           1399 rootCA.crt                                                            
-a----        2/15/2024   5:48 AM           1704 rootCA.key                                                            
-a----        2/25/2024   5:41 PM             42 rootCA.srl      
 
PS C:\Web\University\CA> cat rootCA.crt
-----BEGIN CERTIFICATE-----
MIID2zCCAsOgAwIBAgIULPFQaeEBSoZuCoVP9n/EvYSynY4wDQYJKoZIhvcNAQEL
BQAwfTELMAkGA1UEBhMCVUsxEzARBgNVBAgMClNvbWUtU3RhdGUxFzAVBgNVBAoM
DlVuaXZlcnNpdHkgTHRkMRcwFQYDVQQDDA51bml2ZXJzaXR5Lmh0YjEnMCUGCSqG
SIb3DQEJARYYaGVhZGFkbWluQHVuaXZlcnNpdHkuaHRiMB4XDTI0MDIxNTEzNTEx
N1oXDTI5MDIxMzEzNTExN1owfTELMAkGA1UEBhMCVUsxEzARBgNVBAgMClNvbWUt
U3RhdGUxFzAVBgNVBAoMDlVuaXZlcnNpdHkgTHRkMRcwFQYDVQQDDA51bml2ZXJz
aXR5Lmh0YjEnMCUGCSqGSIb3DQEJARYYaGVhZGFkbWluQHVuaXZlcnNpdHkuaHRi
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnd50RP0SO7Xkz5Yztt1z
cW1nc1oFx5lWYDrZCDXSIUvDOrdCVC5nGdIJIduKHrKeBhVSkUTR1DKNte7RjX9s
RlPra4qFV/0CmB5eBr8ddD6rF8LCYP2tIEHTXlXhYA64x++MIIq5CaGxiTW5dogN
9qnHXo3Po3zizE0KLF3DQGlPNqZZCjIw0+i8zGPjFRMOBkdZafFmnvs3L6NQ3Uk4
J6osqZ+RuEJ6uAIlU9urF8B7NDUHaLIqwBXPWJCzscietDjHq5bslXGX+6pr+iBy
eYbk9YvHYgdNzEljZtU8gT5k0gOeC9xbRdUP9+jjh8e0nYfRy6WdSPIGvpD8NuhU
+QIDAQABo1MwUTAdBgNVHQ4EFgQUwA3KIy0t91zWLrghiiotr5q4X4YwHwYDVR0j
BBgwFoAUwA3KIy0t91zWLrghiiotr5q4X4YwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAQEAgrA9JK4pl4x8gvvx3y6FRZUUK+fj1lqDXhkFHsdOf42u
SI+iC+e/VC+FfUX2A5IB7ObyEKa9x7KnjTL2dQOkxUV40ElecaUFI3tKn/ZmhbK7
RnqIBvLH7uWfy7rrefiMMW9UIYP9EWkXEwKzDPT1BDT8QDpGjMODFmFarehOT6hZ
43WgsXxwg0Smbmph56SDBWUe/gdNKVFk5J8g+qBfU+Ci7++si6WFPTQ/ZfEIoGpv
svRSfBOivVo4un/basHbzUwpnGO+qm5u2eYxiCCXZye17O8BaqagqNYPGzftKfKE
688xD+PVJjj0H80YhA6n0qaL+oO7kUJwCfmBerlH2w==
-----END CERTIFICATE-----
 
PS C:\Web\University\CA> cat rootCA.key
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCd3nRE/RI7teTP
ljO23XNxbWdzWgXHmVZgOtkINdIhS8M6t0JULmcZ0gkh24oesp4GFVKRRNHUMo21
7tGNf2xGU+trioVX/QKYHl4Gvx10PqsXwsJg/a0gQdNeVeFgDrjH74wgirkJobGJ
Nbl2iA32qcdejc+jfOLMTQosXcNAaU82plkKMjDT6LzMY+MVEw4GR1lp8Wae+zcv
o1DdSTgnqiypn5G4Qnq4AiVT26sXwHs0NQdosirAFc9YkLOxyJ60OMerluyVcZf7
qmv6IHJ5huT1i8diB03MSWNm1TyBPmTSA54L3FtF1Q/36OOHx7Sdh9HLpZ1I8ga+
kPw26FT5AgMBAAECggEACD+yRtQL3rmYfvXH1jlgQbggGI2GwYkMM7tDcZvBUurO
5MukUDp7+eQJBN9AKm42FlaJIczNI3ewQqY7xhdXQJ38Ljp20PQIDYuBqXvVNcib
/0H2juOWZxn1aceWPwMJ6e2E/oLNHBkaKhOX8R73DtFMeO7zP2VEQv1QB6UvEXeS
ILuMKzcIoGOeBOjxOiUeInhTpgdyVCtFDun2TkHgw0GCmnXPwzpu8k1vsKCnkO4N
dEcJMrwwxdWcp1DwPsDlylKy5p8goY9feT5KIxkxWxvHEvQiRK8CE34hR3PmemtD
FCXQAMkP0NQxOyufA8VMhtxnEGkAlhU1KDmX1AiPQQKBgQDZYBaszCxmC8ET+S2u
w+rgNz8EA+JHWVJ4zDvXTfl6ksG7X7N/cgqYg0PfYspHj++rVyXd8ni/yz7qkAsv
v02H6wnGGNGjUWxpKXoKBrqRTMFZTCmavXwCWm552PkXZvRAQfI3/t73ALIygE2V
I9yd3/U2mIHreDVkLthB0ZOmQQKBgQC5644enw/xaz2GFEN5GOwgkelKbekxaTfN
88nqOP+xFkjc8VcPy4G9sA5nukNcB5sbXP/NDL+zmc1MKRfGcuxhuUn6I5M53sT0
KiGzmrqTHTsNPzrkHOmCCW47IxxNTHjF/bfiFf5KYkXIMw4ckA7pwvOB/gJrCoMl
j7sdy8kwuQKBgES6ZdDyqSMGmmBnS3NTTILh1NG/LJ8kdbft0wnLBJMlrY1Bv+4O
r5zy+9W4VX502vFQl49qJ5V0fIsLSg4prSjJPojctgOuLdm+HHDpGbnxC7zhINJl
EL1pvD5YRMGKcrWSZDQiKHruRGlFOcJq2UktU0mUxJcfpbnsOcHtsoJBAoGAWt4t
DPxTD3yJimGXBLXQmq6V5gHIKpUxaCCR/D81gTjSDeKK2bZuR+anc70OtQkmnv4a
K/7iJwax11pyO1TqAW1NIcuHZo5hlrCM3jpum/56YSQZRWp4Gk0/qfmkGIeas7Io
0IBNU8+qKYoCiKVEFK5q04rxZZ2ysYYJPyvdvkkCgYAjg8ihJk0tywHrsWReOdHt
5P0hJp6NsjsRwHk6raWTbwEs7eaafVKkWVLB7T5jCshA3nU5/5Wqr5uD5MY/Ctou
jIA32+TSyU0DV57626o2Pis7cIzmcUY2JKmtFUlvPrOROQp/ZxcW3Sm9Ef3hVyGz
JIMTfCnxScVEVWH3AyhAZw==
-----END PRIVATE KEY-----
 
PS C:\Web\University\CA> cat rootCA.srl
6A6B3B653FBA7D80BE108F9824FA1F2EBF502340

The CA directory contains a rootCA keypair. It was likely used to signed the user certificate for authentication in the target web application. It would be rather questionable if this keypair is used system/domain-wise

static


PS C:\Web\University> tree /F /A .\static
tree /F /A .\static
Folder PATH listing
Volume serial number is 000002AF 8E7E:469B
C:\WEB\UNIVERSITY\STATIC
|   db.sqlite3
|   
+---admin
|   +---css
|   |   |   autocomplete.css
|   |   |   base.css
|   |   |   changelists.css
|   |   |   dark_mode.css
|   |   |   dashboard.css
|   |   |   forms.css
|   |   |   login.css
|   |   |   nav_sidebar.css
|   |   |   responsive.css
|   |   |   responsive_rtl.css
|   |   |   rtl.css
|   |   |   widgets.css
|   |   |   
|   |   \---vendor
|   |       \---select2
|   |               LICENSE-SELECT2.md
|   |               select2.css
|   |               select2.min.css
|   |               
|   +---img
|   |   |   calendar-icons.svg
|   |   |   icon-addlink.svg
|   |   |   icon-alert.svg
|   |   |   icon-calendar.svg
|   |   |   icon-changelink.svg
|   |   |   icon-clock.svg
|   |   |   icon-deletelink.svg
|   |   |   icon-no.svg
|   |   |   icon-unknown-alt.svg
|   |   |   icon-unknown.svg
|   |   |   icon-viewlink.svg
|   |   |   icon-yes.svg
|   |   |   inline-delete.svg
|   |   |   LICENSE
|   |   |   README.txt
|   |   |   search.svg
|   |   |   selector-icons.svg
|   |   |   sorting-icons.svg
|   |   |   tooltag-add.svg
|   |   |   tooltag-arrowright.svg
|   |   |   
|   |   \---gis
|   |           move_vertex_off.svg
|   |           move_vertex_on.svg
|   |           
|   \---js
|       |   actions.js
|       |   autocomplete.js
|       |   calendar.js
|       |   cancel.js
|       |   change_form.js
|       |   collapse.js
|       |   core.js
|       |   filters.js
|       |   inlines.js
|       |   jquery.init.js
|       |   nav_sidebar.js
|       |   popup_response.js
|       |   prepopulate.js
|       |   prepopulate_init.js
|       |   SelectBox.js
|       |   SelectFilter2.js
|       |   theme.js
|       |   urlify.js
|       |   
|       +---admin
|       |       DateTimeShortcuts.js
|       |       RelatedObjectLookups.js
|       |       
|       \---vendor
|           +---jquery
|           |       jquery.js
|           |       jquery.min.js
|           |       LICENSE.txt
|           |       
|           +---select2
|           |   |   LICENSE.md
|           |   |   select2.full.js
|           |   |   select2.full.min.js
|           |   |   
|           |   \---i18n
|           |           af.js
|           |           ar.js
|           |           az.js
|           |           bg.js
|           |           bn.js
|           |           bs.js
|           |           ca.js
|           |           cs.js
|           |           da.js
|           |           de.js
|           |           dsb.js
|           |           el.js
|           |           en.js
|           |           es.js
|           |           et.js
|           |           eu.js
|           |           fa.js
|           |           fi.js
|           |           fr.js
|           |           gl.js
|           |           he.js
|           |           hi.js
|           |           hr.js
|           |           hsb.js
|           |           hu.js
|           |           hy.js
|           |           id.js
|           |           is.js
|           |           it.js
|           |           ja.js
|           |           ka.js
|           |           km.js
|           |           ko.js
|           |           lt.js
|           |           lv.js
|           |           mk.js
|           |           ms.js
|           |           nb.js
|           |           ne.js
|           |           nl.js
|           |           pl.js
|           |           ps.js
|           |           pt-BR.js
|           |           pt.js
|           |           ro.js
|           |           ru.js
|           |           sk.js
|           |           sl.js
|           |           sq.js
|           |           sr-Cyrl.js
|           |           sr.js
|           |           sv.js
|           |           th.js
|           |           tk.js
|           |           tr.js
|           |           uk.js
|           |           vi.js
|           |           zh-CN.js
|           |           zh-TW.js
|           |           
|           \---xregexp
|                   LICENSE.txt
|                   xregexp.js
|                   xregexp.min.js
|                   
\---assets
    +---css
    |       animate.min.css
    |       aos.css
    |       bootstrap-icons.css
    |       bootstrap.min.css
    |       dashboard.css
    |       dashboard.css.map
    |       dashboard.scss
    |       dropzone.min.css
    |       fancybox.min.css
    |       flaticon.css
    |       footer.css
    |       footer.css.map
    |       footer.scss
    |       magnific-popup.min.css
    |       meanmenu.css
    |       metismenu.min.css
    |       navbar.css
    |       navbar.css.map
    |       navbar.scss
    |       odometer.min.css
    |       owl.carousel.min.css
    |       owl.theme.default.min.css
    |       remixicon.css
    |       responsive.css
    |       responsive.css.map
    |       responsive.scss
    |       selectize.min.css
    |       simplebar.min.css
    |       style.css
    |       style.css.map
    |       style.scss
    |       templatemo-topic-listing.css
    |       
    +---fonts
    |       bootstrap-icons.woff
    |       bootstrap-icons.woff2
    |       flaticon.eot
    |       flaticon.svg
    |       flaticon.ttf
    |       flaticon.woff
    |       flaticon.woff2
    |       remixicon.eot
    |       remixicon.glyph.json
    |       remixicon.less
    |       remixicon.ttf
    |       remixicon.woff
    |       remixicon.woff2
    |       
    +---images
    |   |   businesswoman-using-tablet-analysis.jpg
    |   |   colleagues-working-cozy-office-medium-shot.jpg
    |   |   faq_graphic.jpg
    |   |   favicon.ico
    |   |   logo.png
    |   |   rear-view-young-college-student.jpg
    |   |   
    |   +---topics
    |   |       colleagues-working-cozy-office-Med-shot.png
    |   |       undraw_Compose_music_re_wpiw.png
    |   |       undraw_Educator_re_ju47.png
    |   |       undraw_Finance_re_gnv2.png
    |   |       undraw_Graduation_re_gthn.png
    |   |       undraw_Group_video_re_btu7.png
    |   |       undraw_happy_music_g6wc.png
    |   |       undraw_online_ad_re_ol62.png
    |   |       undraw_Podcast_audience_re_4i5q.png
    |   |       undraw_Redesign_feedback_re_jvm0.png
    |   |       undraw_Remote_design_team_re_urdx.png
    |   |       undraw_viral_tweet_gndb.png
    |   |       
    |   \---users_profiles
    |           2.png
    |           3.jpg
    |           4.jpg
    |           5.jpg
    |           6.jpg
    |           7.jpeg
    |           7.png
    |           default.png
    |           
    +---js
    |       aos.js
    |       bootstrap.bundle.min.js
    |       ckeditor.js
    |       click-scroll.js
    |       contact-form-script.js
    |       crypto-js.min.js
    |       custom.js
    |       dropzone.min.js
    |       fancybox.min.js
    |       form-validator.min.js
    |       jquery.ajaxchimp.min.js
    |       jquery.appear.js
    |       jquery.magnific-popup.min.js
    |       jquery.meanmenu.js
    |       jquery.min.js
    |       jquery.sticky.js
    |       main.js
    |       metismenu.min.js
    |       odometer.min.js
    |       owl.carousel.min.js
    |       selectize.min.js
    |       simplebar.min.js
    |       sticky-sidebar.min.js
    |       TweenMax.min.js
    |       wow.min.js
    |       
    \---uploads
        +---CSRs
        |       5.csr
        |       7.csr
        |       
        +---lectures
        |       Final.pdf
        |       Final_MlrkzDx.pdf
        |       Introduction.zip
        |       Introduction_1OnecYs.pdf
        |       Introduction_5EI2V6N.pdf
        |       Introduction_AlhP5SY.zip
        |       Introduction_C9gTnh7.zip
        |       Introduction_js8tUqk.zip
        |       Introduction_K03KQx6.pdf
        |       Introduction_kxkxQaD.pdf
        |       Introduction_NipeQ7F.pdf
        |       Introduction_RcgJmpV.pdf
        |       Introduction_SSoPdei.pdf
        |       Introduction_WmlgnJn.pdf
        |       Lecture_1.zip
        |       Lecture_1_1XT4vz8.pdf
        |       Lecture_1_8nIvW6r.pdf
        |       Lecture_1_ApxrD10.pdf
        |       Lecture_1_asYfCuk.pdf
        |       Lecture_1_H9WWf40.pdf
        |       Lecture_1_ODEhS5y.pdf
        |       Lecture_1_R3eXrJg.pdf
        |       Lecture_1_TzqNKFS.zip
        |       Lecture_1_Uknmc7Z.pdf
        |       Lecture_2.zip
        |       Lecture_2_0jZM7aA.pdf
        |       Lecture_2_ajXyOBo.zip
        |       Lecture_2_J98vE7l.pdf
        |       Lecture_2_jt0HAUm.pdf
        |       Lecture_2_K0rTKRT.pdf
        |       Lecture_2_qXVpr8T.pdf
        |       Lecture_2_TAZsMpP.pdf
        |       Lecture_2_W11aioL.zip
        |       Lecture_2_ZMkKb0U.pdf
        |       Lecture_3.zip
        |       Lecture_3_1HpStAM.pdf
        |       Lecture_3_6hM13vA.pdf
        |       Lecture_3_8y7kpIA.pdf
        |       Lecture_3_E8zbku8.zip
        |       Lecture_3_iAm6zMY.zip
        |       Lecture_3_LWfCylC.pdf
        |       Lecture_3_PafSEok.pdf
        |       Lecture_3_pmNY6qw.pdf
        |       Lecture_3_SNtLsz4.zip
        |       Lecture_3_vCwFo21.zip
        |       Lecture_4.zip
        |       Lecture_4_eGdPzAb.pdf
        |       Lecture_4_ExbjtY6.zip
        |       Lecture_4_viQILAn.pdf
        |       Lecture_4_xkuXOUH.pdf
        |       Lecture_4_y5yHLtk.pdf
        |       Lecture_5.pdf
        |       Lecture_5_3lBylH3.pdf
        |       Lecture_5_Zx0Jdq2.pdf
        |       Lecture_6.pdf
        |       Lecture_6_90sbuCq.pdf
        |       Lecture_7.pdf
        |       Lecture_8.pdf
        |       Perfect-Lecture-Sample.zip
        |       
        +---Pub_KEYs
        |       2.asc
        |       3.asc
        |       4.asc
        |       5.asc
        |       6.asc
        |       
        \---signatures
                Final.pdf.sig
                Final.pdf_lGkgLN4.sig
                Introduction.pdf.sig
                Introduction.pdf_3CYSit9.sig
                Introduction.pdf_aevENk4.sig
                Introduction.pdf_BNRkFQy.sig
                Introduction.pdf_EHaX0e9.sig
                Introduction.pdf_ez3Ck0N.sig
                Introduction.pdf_j9HQjl2.sig
                Introduction.pdf_TYTT8n0.sig
                Introduction.pdf_XzFZSxS.sig
                Lecture_1.pdf.sig
                Lecture_1.pdf_6wTlZJN.sig
                Lecture_1.pdf_DD61DLp.sig
                Lecture_1.pdf_E0a2iwg.sig
                Lecture_1.pdf_LyssY9q.sig
                Lecture_1.pdf_ManFIsc.sig
                Lecture_1.pdf_mbgAUjA.sig
                Lecture_1.pdf_OqCzSTp.sig
                Lecture_1.pdf_saJ5iNv.sig
                Lecture_1.pdf_Ze4KwYO.sig
                Lecture_2.pdf.sig
                Lecture_2.pdf_29XUEuJ.sig
                Lecture_2.pdf_atodAJt.sig
                Lecture_2.pdf_NesVcdk.sig
                Lecture_2.pdf_oTq05Ek.sig
                Lecture_2.pdf_OwYdvsr.sig
                Lecture_2.pdf_PNsmeWv.sig
                Lecture_2.pdf_pVjK5Wx.sig
                Lecture_2.pdf_RoDMsiO.sig
                Lecture_2.pdf_VFp6jKV.sig
                Lecture_3.pdf.sig
                Lecture_3.pdf_3CVey0P.sig
                Lecture_3.pdf_cCAJWAe.sig
                Lecture_3.pdf_lxG83A6.sig
                Lecture_3.pdf_RkkrZBy.sig
                Lecture_3.pdf_SoYK3qM.sig
                Lecture_3.pdf_toX8vUb.sig
                Lecture_3.zip.sig
                Lecture_4.pdf.sig
                Lecture_4.pdf_A5GL2rQ.sig
                Lecture_4.pdf_jP93tcb.sig
                Lecture_4.pdf_lGiR3ZL.sig
                Lecture_4.pdf_otlIj84.sig
                Lecture_5.pdf.sig
                Lecture_5.pdf_eqrAnLi.sig
                Lecture_5.pdf_z3y05cg.sig
                Lecture_6.pdf.sig
                Lecture_6.pdf_XQpqHon.sig
                Lecture_7.pdf.sig
                Lecture_8.pdf.sig
                Perfect-Lecture-Sample.zip.sig

There are 2 CSR files in the static/assets/uploads/CSRs directory;

  • 5.csr
  • 7.csr

The assets\uploads\Pub_KEYs directory contains Public PGP key blocks;

  • 2.asc
  • 3.asc
  • 4.asc
  • 5.asc
  • 6.asc

5.csr


*Evil-WinRM* PS C:\Web\University\static\assets\uploads\CSRs> cat 5.csr
-----BEGIN CERTIFICATE REQUEST-----
MIICyTCCAbECAQAwgYMxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRl
MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFDASBgNVBAMMC21h
cnRpbi5yb3NlMSYwJAYJKoZIhvcNAQkBFhdtYXJ0aW4ucm9zZUBob3RtYWlsLmNv
bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANogIhiJao2d/3oEOzRS
7523b4Aat8EsnYzVhGpbtARGPmLYJYIZaBmqhAUt3tFIkWwGa0wyGFodH6E2gPqh
2NO6E9wRsPFSR0BcBj3KoanMVzLr34K6/mAOgNRxCgMAiCB1LiZF7nSPoax3HifL
690sB9cRwqzMSsF0yuYX1/1uPdg/WrB+dfDgGIvzbFaTUVXvgNHGhBq4W5lOQUlB
tLf8UgD9Hagu9uZw8Nd7MobqmFvZ7qWmVtKwBecNTzHAUu+t30rZawHp29DGT6jC
fT0heaIjWDyPhQWA/kbNQw6KuafyeWP3cfxAr97GYswiWBlv0NaDa4E073IxJdh6
llUCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQAURkomwJlWrbzOoZZi2bshV/x2
gOWrO499UBslwuBjYihy3e75KKjgMzLXA+q+9BKgUPpS3J88tWLha/Xu427PgHFG
vxV9irgwvzaY0IwdouAu7Ok6czbmWilM4cyrsgty1V/d6LUU4D03y6Z2QATfPKA+
epAy70Tmg8BKVOcYJMDyPjxutu2chMqPxISDKRIAHnG2MFb37oxsUrzs1KGDR609
V9450uplSyDukY6HjwzNA+Hy81E06PZimGQzRcmBJ79TwwEWd8fhiKe94VDbkrJm
Ou5n75i426bj58sgfcMTGTmWNGXDvpOehGeCus26SebC+jyy1LteZT02oWk3
-----END CERTIFICATE REQUEST-----
 
┌──(kali㉿kali)-[~//htb/labs/university/certs]
└─$ openssl req -in 5.csr -text -noout | grep -w 'Subject:'
        Subject: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=martin.rose, emailAddress=martin.rose@hotmail.com

The 5.csr file belongs to the martin rose

7.csr


 
*Evil-WinRM* PS C:\Web\University\static\assets\uploads\CSRs> cat 7.csr
-----BEGIN CERTIFICATE REQUEST-----
MIICvjCCAaYCAQAweTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAwwIU3Rl
dmVuLlUxHzAdBgkqhkiG9w0BCQEWEHN0ZXZlbkB5YWhvby5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3k0uVdocW8NetEK1r0upXF69HsbYPwUM6
0WfPS4Xx/00udkNlEJqpTsOI/nIoO3haq3P5G35T2m1hg959BGE30C3jkCL3znjc
VIPOe8jvsqJ43n52iIIbD6za66jyzhXezFpBpWJVfOu1tpk9Ybe9ZOJAuSsY7Zpa
DipcPgdVVfIQz/pwZMJhmjL4YdFhPvHOpoE0mHwkqZwnWjOwJa9b1hoGbEcnCyoo
8G3UZNIOG+J97YJ4jMQpFLB5A4x+IbOfRboR18cLRUSZBm1r9Scz4ZM81WEV8sl+
OFAQd6AV2SPCCHCsPwDjebw07bOlrQlrDbX0G0gybPxtKTztvUSlAgMBAAGgADAN
BgkqhkiG9w0BAQsFAAOCAQEAp3QlBtHiekDgns+UZjRnDhuFjDsug94dfwz7Somw
4Pqu4VPSMwKhb+5NhkbFSnZnZouIOkU+DThL6LcvFGYz6ymGB7u+ueKZtuYzaCkt
Ysvpd27DzcYcrHzVIKEGOJCLN4TX5fn56i+8U0t/1jjNx3mAFCxFaDphGxwbt8bB
G7qgu5IxWMiAbYAkYXz4VpqlLQSRxieZpOYDEN7ww8SsTxPNET1/kQQI/2I3PQsq
w7INxKIMIC+RcbfIH0lWqWiXN8QbX80BR4dFb5Eysi+C6YXBiYX8DOssMV8SuS6x
vhfFC+DaGw2JWmloFzhUIA7RINVtf1Oe303fkjeHYRRx5g==
-----END CERTIFICATE REQUEST-----
 
┌──(kali㉿kali)-[~//htb/labs/university/certs]
└─$ openssl req -in 7.csr -text -noout | grep -w 'Subject:'
        Subject: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=Steven.U, emailAddress=steven@yahoo.com

The 7.csr file belongs to Steven.U

University


PS C:\Web\University> cd University ; ls
 
 
    Directory: C:\Web\University\University
 
 
Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
d-----         3/4/2024   4:09 PM                migrations                                                            
d-----        3/13/2024  11:17 AM                templates                                                             
d-----         3/9/2024  11:39 PM                __pycache__                                                           
-a----         2/7/2024  12:39 AM            237 admin.py                                                              
-a----        12/3/2023   4:28 AM            397 asgi.py                                                               
-a----        2/15/2024   9:08 AM           2364 certificate_utils.py                                                  
-a----       10/19/2023   8:10 PM            436 customMiddlewares.py                                                  
-a----        2/15/2024   9:42 AM           3278 custom_decorators.py                                                  
-a----        2/15/2024  12:48 PM           8296 forms.py                                                              
-a----         3/4/2024   4:07 PM           3979 models.py                                                             
-a----        2/25/2024   4:56 PM           3479 settings.py                                                           
-a----         3/4/2024   8:27 PM           1893 urls.py                                                               
-a----       10/15/2024  11:27 AM          23410 views.py                                                              
-a----        12/3/2023   4:28 AM            397 wsgi.py                                                               
-a----        12/3/2023   4:28 AM              0 __init__.py                                                           

This appears to be the backend directory of the target web application

settings.py


Evil-WinRM* PS C:\Web\University\University> cat settings.py
"""
Django settings for University project.
 
Generated by 'django-admin startproject' using Django 4.2.7.
 
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
 
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
 
from pathlib import Path
import os
 
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
 
 
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
 
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-fs-2bin)f_nd1q5jly9_g8$9e$2y2_zy!pn=*qji^i*-v5yt7#'
 
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
 
ALLOWED_HOSTS = ['university.htb']
 
 
# Application definition
 
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'University',
]
 
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
 
ROOT_URLCONF = 'University.urls'
 
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
 
WSGI_APPLICATION = 'University.wsgi.application'
 
 
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
 
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
 
 
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
 
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
 
 
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
 
LANGUAGE_CODE = 'en-us'
 
TIME_ZONE = 'UTC'
 
USE_I18N = True
 
USE_TZ = True
 
 
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
 
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
 
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
 
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 
AUTH_USER_MODEL = 'University.CustomUser'
 
rooCA_Cert = os.path.join(BASE_DIR, 'CA/rootCA.crt')
rooCA_PrivKey = os.path.join(BASE_DIR, 'CA/rootCA.key')

The settings.py file appears to set environment variables

views.py


*Evil-WinRM* PS C:\Web\University\University> cat views.py
from .certificate_utils import *
from .custom_decorators import course_owner_required, professor_required, PSC_required, PubKeyring_required, student_required
from .forms import *
from .models import *
from base64 import urlsafe_b64encode
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate, login
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.views import LogoutView
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.http import HttpResponseRedirect, HttpResponseBadRequest
from django.shortcuts import render, redirect, get_object_or_404
from django.template.loader import get_template
from django.urls import reverse, reverse_lazy
from django.utils.encoding import force_bytes
from django.conf import settings
import random
import string
import subprocess
 
 
def index(request):
    return render(request, 'index.html')
 
 
 
def contact(request):
    if request.method == "POST":
        if ("name" in request.POST) and ("email" in request.POST) and ("title" in request.POST) and ("message" in request.POST):
            messages.success(request,"we have received your message, our team will review it and contact you as soon as possible")
        else:
            messages.error(request, "an error has occured... please check your submission and try again.")
    context = {"title": "Contact Us"}
    return render(request, 'contact.html', context)
 
 
 
def page_not_found(request, exception):
    context = {"title": "Page Not Found"}
    return render(request, 'error-404.html', context,status=404)
 
 
 
class MyLogoutView(LogoutView):
    next_page = reverse_lazy('auth_login')
 
    def get_next_url(self):
        return self.next_page
 
 
 
def Student_register(request):
    if request.method == 'POST':
        form = StudentCreationForm(request.POST)
        if form.is_valid():
            # process the form data
            user = form.save(commit=False)
            # set the user_type field to Student
            user.user_type = "Student"
            # save the user
            user.save()
            return redirect(reverse('auth_login'))
        else:
            # form is not valid
            context = {"title": "Student Register", "form": form}
    else:
        form = StudentCreationForm()
        context = {"title": "Student Register", "form": form}
    return render(request, 'register.html', context)
 
 
 
def Professor_register(request):
    if request.method == 'POST':
        form = ProfessorCreationForm(request.POST)
        if form.is_valid():
            # process the form data
            user = form.save(commit=False)
            # set the user_type field to Freelancer
            user.user_type = "Professor"
            # Professor account should be activated be the admin after the review
            user.is_active = False
            # save the user
            user.save()
            return redirect(reverse('auth_login'))
        else:
            # form is not valid
            context = {"title": "Professor Register", "form": form}
    else:
        messages.success(request, 'Note: After creating your "Professor" account, your account will be inactive until our team reviews your account details and contacts you by email to activate your account.')
        form = ProfessorCreationForm()
        context = {"title": "Professor Register", "form": form}
    return render(request, 'register.html', context)
 
 
 
def auth_login(request):
    if request.method == 'POST':
        # check if account is deactivated
        if CustomUser.objects.filter(username=request.POST["username"]):
            user = CustomUser.objects.get(username=request.POST["username"])
            if not user.is_active:
                form = AuthenticationForm()
                messages.error(request, "Sorry, this account is not activated and can not be authenticated!.")
                return render(request, 'login.html', {'form': form, 'title': 'Login'})
        # check creds
        form = AuthenticationForm(data=request.POST)
        if form.is_valid():
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            user = authenticate(request, username=username, password=password)
            if user is not None:
                login(request, user)
                user.reset_failed_login_attempts()
                if 'next' in request.GET:
                    return HttpResponseRedirect(request.GET["next"])
 
                return redirect(reverse('profile'))
        else:
            # if the user account exists --> increase the "failed_login_attempts"
            try:
                user = CustomUser.objects.get(
                    username=request.POST["username"])
                user.increment_failed_login_attempts()
                if user.failed_login_attempts == 10:
                    messages.error(request, "Due to multiple bad login attempts, we have deactivated your account...")
                    messages.error(request, "To re-activate your account you have to provide the valid security answers for your account.")
                    return redirect(reverse('account_recovery'))
            except ObjectDoesNotExist:
                pass
    else:
        form = AuthenticationForm()
    return render(request, 'login.html', {'form': form, 'title': 'Login'})
 
 
 
def auth_SDC_login(request):
    if request.method == 'POST':
        if 'SDC' not in request.FILES or request.FILES["SDC"].size == 0:
            return HttpResponseBadRequest("ERROR: Malicious behavior has been detected!")
 
        form = SDC_Login_Form(request.POST, request.FILES)
        if form.is_valid():
            SDC = form.cleaned_data.get('SDC')
            SDC_data = SDC.read()
            # try to verify the certificate
            if not verify_signed_Cert(SDC_data):
                return HttpResponseBadRequest("ERROR: Invalid signed certificate file!")
 
            username ,email = get_certificate_details(SDC_data)
            user = CustomUser.objects.filter(username = username, email=email)
            if user.exists():
                login(request, user.get())
                return redirect(reverse('profile'))
            else:
                return HttpResponseBadRequest('Invalid user primary key!')
        else:
                errors = form.errors.as_data()
                for field, error_list in errors.items():
                    for error in error_list:
                        messages.error(request, error.message)
    else:
        form = SDC_Login_Form()
 
    context = {
        'form':form,
        'title': 'Signed Digital Certificate Login',
    }
    return render(request, 'sdc_login.html', context)
 
 
 
@login_required
def profile(request):
    if request.method == 'POST':
        # determine the user type
        if request.user.user_type == "Student":
            user = Student.objects.get(id=request.user.id)
            form = StudentProfileForm(request.POST, instance=user)
        elif request.user.user_type == "Professor":
            user = Professor.objects.get(id=request.user.id)
            form = ProfessorProfileForm(request.POST, instance=user)
        if form.is_valid():
            form.save()
            messages.success(request, 'Your profile has been updated successfully.')
            # returning to the function will render the profile page again with the updates accquired
            return redirect('profile')
    else:
        if request.user.user_type == "Student":
            user = Student.objects.get(id=request.user.id)
            form = StudentProfileForm(instance=user)
        elif request.user.user_type == "Professor":
            user = Professor.objects.get(id=request.user.id)
            form = ProfessorProfileForm(instance=user)
    context = {
        'form': form,
        'title': 'My Profile',
        'user': user,
    }
    return render(request, 'profile.html', context)
 
 
 
@login_required
def pdf_profile(request):
    if request.user.user_type == "Student":
        user = Student.objects.get(id=request.user.id)
        form = StudentProfileForm(instance=user)
        #get student enrolled in courses
        try:
            courses = user.courses.all()
        except:
            courses = []
    elif request.user.user_type == "Professor":
        user = Professor.objects.get(id=request.user.id)
        form = ProfessorProfileForm(instance=user)
        #get professor created courses
        try:
            courses = Course.objects.filter(teacher=user)
        except:
            courses = []
    else:
        return HttpResponseBadRequest("ERROR: Malicious behavior has been detected!")
    context = {
        'form': form,
        'user': user,
        'courses':courses,
    }
    template = get_template('pdf_profile.html')
    html = template.render(context)
    random_prefix = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
    with open(f"{random_prefix}.html", "w") as html_file:
        html_file.write(html)
    try:
        subprocess.run(["C:\\Program Files\\Python310\\Scripts\\xhtml2pdf.exe", f"{random_prefix}.html"], shell=False, check=False)
    except subprocess.CalledProcessError as e:
        print(f"Command execution failed: {e.output}")
        return None
    pdf_file = open(f"{random_prefix}.pdf", 'rb').read()
    response = HttpResponse(pdf_file, content_type='application/pdf')
    response['Content-Disposition'] = 'attachment; filename="profile.pdf"'
    # get rid of the temp files
    os.remove(f"{random_prefix}.html")
    os.remove(f"{random_prefix}.pdf")
    return response
 
 
 
def visit_profile(request, user_id):
    if not user_id.isdigit():
        return HttpResponseBadRequest("WARNING: Malicious input has been detected!")
    custom_user = get_object_or_404(CustomUser, id=user_id)
    if custom_user.user_type == "Student":
        user = Student.objects.get(id=custom_user.id)
        form = StudentProfileForm(instance=user)
        #get student enrolled in courses
        try:
            courses = user.courses.all()
        except:
            courses = []
    elif custom_user.user_type == "Professor":
        user = Professor.objects.get(id=custom_user.id)
        form = ProfessorProfileForm(instance=user)
        #get professor created courses
        try:
            courses = Course.objects.filter(teacher=user)
        except:
            courses = []
    else:
        return HttpResponseBadRequest("ERROR: Malicious behavior has been detected!")
 
    context = {
        'form': form,
        'title': f'{user.username} Profile',
        'user': user,
        'courses':courses,
    }
    return render(request, 'visit_profile.html', context)
 
 
 
@login_required
def upload_profile_image(request):
    if request.method == 'POST':
        user = CustomUser.objects.get(id=request.user.id)
        if 'image' in request.FILES:
            form = ProfileImageForm(request.POST, request.FILES, instance=user)
            if form.is_valid():
                if request.user.image and 'default.png' not in request.user.image.path:
                    old_image_path = request.user.image.path
                    os.remove(old_image_path)
                form.save()
                messages.success(request, 'Profile image uploaded successfully.')
            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:
            return HttpResponseBadRequest("WARNING: Malicious behavior has been detected!")
    else:
        return HttpResponseBadRequest("WARNING: Malicious behavior has been detected!")
    return redirect(reverse('profile'))
 
 
@login_required
@professor_required
def upload_public_key(request):
    if request.method == 'POST':
        user = Professor.objects.get(id=request.user.id)
        if user.public_key:
            old_public_key_path = user.public_key.path
        form = Professor_PubKEY_Form(request.POST, request.FILES, instance=user)
        if form.is_valid():
            if user.public_key:
                try:
                    os.remove(old_public_key_path)
                except:
                    pass
            form.save()
            messages.success(request, 'Public Key uploaded successfully.')
        else:
            # display form with error messages
            errors = form.errors.as_data()
            for _, error_list in errors.items():
                for error in error_list:
                    messages.error(request, error.message)
    else:
        form = Professor_PubKEY_Form()
    context = {
        'title': 'GPG key upload',
        'form' : form
    }
    return render(request, 'upload-gpg-pubKey.html', context)
 
 
 
@login_required
def course(request):
    if "page_number" in request.GET and request.GET["page_number"].isdigit() and int(request.GET["page_number"]) > 0:
        page_number = request.GET["page_number"]
    else:
        page_number=1
    # intially get all the courses
    courses = Course.objects.order_by('id')
    # Create a Paginator object
    paginator = Paginator(courses, 7)
    # Get the desired page
    courses = paginator.get_page(page_number)
    context = {'title': 'Course dashboard', 'courses': courses, 'page_number':page_number, 'user': request.user}
    return render(request, 'course-dashboard.html', context)
 
 
 
@login_required
@professor_required
def my_courses(request):
    professor = Professor.objects.get(id=request.user.id)
    # intially get all the courses
    courses = Course.objects.filter(teacher=professor)
    context = {'title': 'My Courses', 'courses': courses,'page_number': 1, 'user': professor}
    return render(request, 'course-dashboard.html', context)
 
 
 
@login_required
@PSC_required
def create_course(request):
    form = CourseForm(request.POST or None)
    if request.method == 'POST':
        if form.is_valid():
            course = form.save(commit=False)
            course.teacher = Professor.objects.get(customuser_ptr_id=request.user.id)
            course.save()
            messages.success(request, 'New Course created successfully!')
            return redirect(reverse('course_details', 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)
    context = {'form': form, 'title': 'Create Course', 'user': request.user}
    return render(request, 'create_course.html', context)
 
 
@login_required
@PSC_required
@course_owner_required
def edit_course(request, course_id):
    if not course_id.isdigit():
        return HttpResponseBadRequest("WARNING: Malicious input has been detected!")
    course = get_object_or_404(Course, id=course_id)
    if request.method == 'POST':
        form = CourseForm(request.POST, instance=course)
        if form.is_valid():
            form.save()
            messages.success(request, 'Your course details has been updated successfully.')
            return redirect(reverse('course_details',kwargs={'course_id':course_id}))
    else:
        form = CourseForm(instance=course)
    context = {
        'form': form,
        'title': 'Edit course details',
    }
    return render(request, 'create_course.html', context)
 
 
 
@login_required
@student_required
def course_enroll(request, course_id):
    if not course_id.isdigit():
        return HttpResponseBadRequest("WARNING: Malicious input has been detected!")
    course = get_object_or_404(Course, id=course_id)
    student = Student.objects.get(id=request.user.id)
    if student not in course.students.all():
        course.students.add(student)
        student.courses.add(course)
 
    return redirect(reverse('course_details',kwargs={'course_id':course_id}))
 
 
 
@login_required
def course_details(request, course_id):
    if course_id.isdigit():
        course = get_object_or_404(Course, id=course_id)
        # assume the Student is not enrolled initially
        Enrolled = False
        Owner = False
        lectures = []
        # let's assume he is Professor in the begginning
        if request.user.user_type == "Student":
            student = Student.objects.get(id=request.user.id)
            # Check if the Student has already enrolled
            if student in course.students.all():
                Enrolled = True
                lectures = Lecture.objects.filter(course=course, is_verified=True)
 
        elif request.user.user_type == "Professor":
            if course.teacher.id == request.user.id:
                Owner = True
 
        context = {
            "title": course.title + " - Course Details",
            "course": course,
            "lectures": lectures,
            "Enrolled": Enrolled,
            "Owner": Owner,
            "user": request.user,
            }
        return render(request, 'course-details.html', context)
 
    else:
        return HttpResponseBadRequest("WARNING: Malicious content has been detected!")
 
 
 
@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)
            file_name = request.FILES['file'].name
            file_path = os.path.join(settings.STATIC_ROOT, 'assets', 'uploads', 'lectures', file_name)
            if os.path.exists(file_path):
                messages.error(request, f'Error: A file named "{file_name}" already exists.')
                context = {'form': form, 'title': 'Upload a new Lecture', 'user': request.user}
                return render(request, 'upload_lecture.html', context)
            else:
                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)
 
 
 
'''
@login_required
@professor_required
@PubKeyring_required
def upload_signed_file(request):
    if request.method == 'POST':
        if 'file' in request.FILES:
            form = DocumentForm(request.POST, request.FILES)
            if form.is_valid():
                obj = form.save(commit=False)
                user = Professor.objects.get(id=request.user.id)
                obj.professor = user
                obj.save()
                verification = form.verify_integrity(obj.DigiSign.path, obj.file.path, user.public_key.path)
                if not verification.valid:
                    obj.delete()
                    os.remove(obj.DigiSign.path)
                    os.remove(obj.file.path)
                    messages.error(request, 'Error: Invalid File Integrity!')
                else:
                    obj.date = datetime.fromtimestamp(int(verification.timestamp))
                    obj.save()
                    messages.success(request, 'Signed File uploaded successfully.')
            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)
 
            return(redirect('upload_signed_file'))
        else:
            return HttpResponseBadRequest("WARNING: Malicious behavior has been detected!")
    else:
        form = DocumentForm()
        context = {
            'form': form,
            'title': 'Upload Signed File',
        }
        return render(request, 'upload.html', context)
'''
 
 
 
@login_required
def request_signed_cert(request):
    if request.method == 'POST':
        if 'csr' in request.FILES:
            form = CSR_form(request.POST, request.FILES, instance=request.user)
            if form.is_valid():
                if request.user.csr:
                    try:
                        os.remove(request.user.csr.path)
                    except:
                        pass
                form.save()
                # check if the CSR info matches professor information
                try:
                    common_name, email_address = get_csr_details(request.user.csr.path)
                except:
                    return HttpResponseBadRequest("WARNING: Malicious behavior has been detected!")
 
                if common_name == request.user.username and email_address == request.user.email:
                    return generate_signed_cert(request, request.user)
                else:
                    return HttpResponseBadRequest("ERROR: CSR information dose not match your database records!")
            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:
            return HttpResponseBadRequest("WARNING: Malicious behavior has been detected!")
    else:
        form = CSR_form()
        context = {
            'form': form,
            'title': 'Request a signed certificate',
        }
        return render(request, 'request-signed-certificate.html', context)
    return redirect(reverse('request_signed_cert'))
 
 
 
def get_lectures_list(request):
    lectures = Lecture.objects.filter(is_verified=False, Evaluating=False)
    lectures_list = ''
    for lecture in lectures:
        lectures_list += lecture.file.path.split("\\")[-1] + '\n'
        lecture.Evaluating = True
        lecture.save()
    return HttpResponse(lectures_list)

Checking the views.py file reveals the inner working of the web application

certificate_utils.py


*Evil-WinRM* PS C:\Web\University\University> cat certificate_utils.py
from .settings import rooCA_Cert, rooCA_PrivKey
from base64 import b64encode
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from django.http import HttpResponse, HttpResponseBadRequest
import subprocess
 
def get_csr_details(csr_path):
    with open(csr_path, "rb") as csr_file:
        csr = x509.load_pem_x509_csr(csr_file.read(), default_backend())
        subject = csr.subject
        common_name = subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value
        email_address = subject.get_attributes_for_oid(x509.NameOID.EMAIL_ADDRESS)[0].value
    return common_name, email_address
 
 
 
def verify_signed_Cert(signed_cert_bytes):
    with open(rooCA_Cert, "rb") as ca_file:
        ca_certificate = x509.load_pem_x509_certificate(ca_file.read(), default_backend())
    try:
        signed_certificate = x509.load_pem_x509_certificate(signed_cert_bytes, default_backend())
        ca_certificate.public_key().verify(
            signed_certificate.signature,
            signed_certificate.tbs_certificate_bytes,
            padding.PKCS1v15(),
            signed_certificate.signature_hash_algorithm,
        )
        return True
    except:
        return False
 
 
 
def get_certificate_details(cert_bytes):
    cert = x509.load_pem_x509_certificate(cert_bytes, default_backend())
    subject = cert.subject
    common_name = subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value
    email_address = subject.get_attributes_for_oid(x509.NameOID.EMAIL_ADDRESS)[0].value
    return common_name, email_address
 
 
 
def generate_signed_cert(request, user):
    command = r'"C:\\Program Files\\openssl-3.0\\x64\\bin\\openssl.exe" x509 -req -in "{}" -CA "{}" -CAkey "{}" -CAcreateserial'.format(user.csr.path, rooCA_Cert, rooCA_PrivKey)
    output = subprocess.check_output(command, shell=True).decode()
    # Base64 encode the content of the file
    encoded_output = b64encode(output.encode()).decode()
    response = HttpResponse(content_type='application/x-x509-ca-cert')
    # Set the content of the file as a cookie
    response.set_cookie('PSC', encoded_output)
    response['Content-Disposition'] = 'attachment; filename="signed-cert.pem"'
    response.write(output)
    return response

The certificate_utils.py file is responsible for dealing with certificates including signing and issuing

It uses the following command;

"C:\\Program Files\\openssl-3.0\\x64\\bin\\openssl.exe" x509 -req -in "{}" -CA "{}" -CAkey "{}" -CAcreateserial

This may be used for certificate forgery