Subversion (SVN)


A Subversion (SVN) repository has been discovered within the target web server, specifically located at the /svn/dev/ directory. Initially, access to the /svn endpoint was restricted by Basic HTTP authentication. Upon attempting authentication using the default credential (admin:admin), the server responded with a 403 Forbidden status code. This initially suggested that further exploration might be futile.

However, through directory fuzzing, it was revealed that the /svn/dev/ subdirectory exists within the /svn/ path. This discovery indicates that despite the initial restriction, deeper reconnaissance can uncover additional accessible resources within the SVN repository.

info


┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/phobos/svn]
└─$ svn info http://admin@$IP:80/svn/dev
Path: dev
URL: http://admin@192.168.104.131/svn/dev
Relative URL: ^/
Repository Root: http://admin@192.168.104.131/svn/dev
Repository UUID: 48a68f27-ba40-4519-a39f-bccbce31f2f4
Revision: 3
Node Kind: directory
Last Changed Author: admin
Last Changed Rev: 3
Last Changed Date: 2021-01-26 16:26:06 +0100 (Tue, 26 Jan 2021)

The current repository has been “revisioned” 5 times

ls


┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/phobos/svn]
└─$ svn ls http://admin@$IP:80/svn/dev
internal/
manage.py
static/
submissions/
users/

As seen in the web server, there exist what appears to be source code of a Python web application. Those also correspond to the mapping results from ffuf earlier

log


┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/phobos/svn]
└─$ svn log http://admin@$IP:80/svn/dev
------------------------------------------------------------------------
r3 | admin | 2021-01-26 16:26:06 +0100 (Tue, 26 Jan 2021) | 1 line
 
 
------------------------------------------------------------------------
r2 | admin | 2021-01-26 16:25:43 +0100 (Tue, 26 Jan 2021) | 1 line
 
Commit 2
------------------------------------------------------------------------
r1 | admin | 2021-01-26 16:25:37 +0100 (Tue, 26 Jan 2021) | 1 line
 
Created repository
------------------------------------------------------------------------

Not much information is provided to the commit log

Reversion


Reverting the current repository to older revisions is easily accomplished using the up command in svn. This command efficiently navigates through the repository’s history, allowing users to explore and extract specific versions as needed

r3


┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/phobos/svn]
└─$ svn checkout http://admin@$IP:80/svn/dev
 
[...REDACTED...]
 
Checked out revision 3.

In order to use the up command in svn, the repository must be locally available. For that, I will download the current repository to Kali using the checkout command

┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ cd ./dev ; ll
total 32K
4.0K drwxrwxr-x 3 kali kali 4.0K Mar  9 16:01 ..
4.0K drwxrwxr-x 4 kali kali 4.0K Mar  9 16:00 users
4.0K drwxrwxr-x 7 kali kali 4.0K Mar  9 16:00 .
4.0K -rwxrwxr-x 1 kali kali  629 Mar  9 16:00 manage.py
4.0K drwxrwxr-x 2 kali kali 4.0K Mar  9 16:00 submissions
4.0K drwxrwxr-x 7 kali kali 4.0K Mar  9 16:00 static
4.0K drwxrwxr-x 2 kali kali 4.0K Mar  9 16:00 internal
4.0K drwxrwxr-x 4 kali kali 4.0K Mar  9 16:00 .svn

The repository has been downloaded to Kali The notable aspect is the presence of the .svn directory

Similar to the .git directory in Git, the .svn directory in Apache Subversion (SVN) serves as a crucial repository metadata storage. Understanding the contents of the .svn directory is paramount, offering insights into repository structure and potentially exposing sensitive information.

manage.py


┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ cat manage.py
#!/usr/bin/env python3
"""Django's command-line utility for administrative tasks."""
import os
import sys
 
 
def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'internal.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()

It’s a Django app

users/views.py


┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ cat users/views.py 
from django.shortcuts import render,redirect,HttpResponse
from django.contrib import messages
import subprocess
import string
import os,re
 
from django.contrib.auth.models import User
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
from .forms import UserRegistrationForm,Passwordresetform
from django.contrib.auth import authenticate,logout
# Create your views here.
 
 
 
 
def register(request):
    form=UserRegistrationForm()
    context={'form':form}
    if(request.method=="POST"):
        try:
            form=UserRegistrationForm(request.POST)
            if(form.is_valid):
                form.save()
                return redirect('login')
        except ValueError:
            messages.error(request,'Pls check your input fields')
            return redirect('register')
             
 
    return render(request,template_name='registration.html',context=context)        
 
 
 
@login_required
def home(request):
    return render(request,template_name='home.html')
 
@login_required
def account(request):
    form=Passwordresetform()
    context={'form':form}
    if(request.method=="POST"):
        form=Passwordresetform(request.POST)
        if(form.is_valid()):
            print(form.cleaned_data.get('username'))
            user=User.objects.get(username=request.POST['username'])
            if(form.cleaned_data.get('newpass')==form.cleaned_data.get('cnewpass')):
                user.set_password(form.cleaned_data.get('newpass'))
                user.save()
                messages.success(request,message='Password successfully changed')
                return redirect('home')
    return render(request,template_name='account.html',context=context)
 
@login_required
def jobs(request):
    if request.method=='POST':
        messages.success(request,message='Your work have been submitted to admin he will review it so please wait')
        return redirect('home')
    return render(request,template_name='jobs.html')
 
@staff_member_required
def remove_view_submissions(request):
    if(request.method=="POST"):
        action=request.POST["action"]
        if(action=="view"):
            f=request.POST["file"]       
            fil=open('/var/www/html/internal/submissions/'+f,'r')
            print(f)
            output=fil.read()
            return HttpResponse(content=output)
 
 
        elif(action=="delete"):
            cmd=["rm","/var/www/html/internal/submissions/{}".format(request.POST["file"])]
            cmd="/bin/bash -c 'rm /var/www/html/internal/submissions/{}'".format(request.POST["file"])
            print(cmd)
            a=os.system(cmd)
            messages.info(request,message="The file has been deleted") 
 
    files=subprocess.Popen(['ls','/var/www/html/internal/submissions'],stdout=subprocess.PIPE).stdout.read().decode().split('\n')
    print(files)    
    context={"files":files}
    return render(request,template_name='submissions.html',context=context)

The users/views.py file shows the following endpoints:

  • /register
  • /login
  • /home
  • /account
  • /jobs
  • /submissions
Broken File Upload

The file upload feature is not implemented properly

Vulnerability (Password Reset)

While it would appear that the web application supports file upload and uploaded files gets executed, there is a flaw in the account function It does not check for old password. This would mean anybody can reset the password of any existing user

Vulnerability (Code Execution)

There is a OS command injection vulnerability present if the delete value is supplied to the action parameter. The user is able to control the file parameter, leading to OS command injection.

internal/settings.py


┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ cat internal/settings.py | grep -v '^#'
"""
Django settings for internal project.
 
Generated by 'django-admin startproject' using Django 2.2.16.
 
For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/
 
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
 
import os
 
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
 
 
SECRET_KEY = 'w7hj11z7mg+cotad702-+vz)gd==xz=8zd=k4=eq8ir2@36%*('
 
DEBUG = False
 
ALLOWED_HOSTS = ['*']
 
 
 
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users',
    'widget_tweaks'
]
 
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 = 'internal.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 = 'internal.wsgi.application'
 
 
 
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
 
 
 
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',
    },
]
 
 
 
LANGUAGE_CODE = 'en-us'
 
TIME_ZONE = 'UTC'
 
USE_I18N = True
 
USE_L10N = True
 
USE_TZ = True
 
 
 
STATIC_URL = '/static/'
 
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
 
LOGIN_URL='login'
LOGIN_REDIRECT_URL = 'home'

The internal/settings.py file reveals the app key; w7hj11z7mg+cotad702-+vz)gd==xz=8zd=k4=eq8ir2@36%*( The backend DB is sqlite3

internal/urls.py


┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ cat internal/urls.py
"""internal URL Configuration
 
The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.contrib.auth.views import LogoutView,LoginView
from users.views import register,home,account,jobs,remove_view_submissions
urlpatterns = [
    path('',LoginView.as_view(template_name='login.html')),
    path('home/',home,name='home'),
    path('login/',LoginView.as_view(template_name='login.html'),name='login'),
    path('logout/',LogoutView.as_view(template_name='logout.html'),name='logout'),
    path('register/',register,name='register'),
    path('account/',account,name='account'),
    path('jobs',jobs,name='jobs'),
    path('submissions/',remove_view_submissions,name="submission")
 
]
#path('admin/', admin.site.urls)

The internal/urls.py file also reveals the routing There is also a comment that suggesting that there might be an admin page; admin/

r2


┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ svn up -r 2                            
Updating '.':
A    todo
Updated to revision 2.

Reverting to the 2nd commit shows that the todo file was appended

todo


┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ cat todo                               
*Change this application to a this virtual host internal-phobos.phobos.offsec
*Randomise the secret key
* Make a database for maintaining employee ssh credentials
* Move the entire site to a docker container
* Configure the ufw firewall

The todo file reveals a hidden virtual host for an internal application; internal-phobos.phobos.offsec

  • There is also a mention of making a database for SSH credentials
  • The application might be running inside a Docker container
  • The target environment has firewall enabled

The virtual host has been appended to the /etc/hosts file on Kali for local DNS resolution

r1


┌──(kali㉿kali)-[~/…/PG_PRACTICE/phobos/svn/dev]
└─$ svn up -r 1
Updating '.':
D    todo
Updated to revision 1.

Switching to the first commit deletes the todo file