Home Code Security Advent Calendar 2020 Answers
Post
Cancel

Code Security Advent Calendar 2020 Answers

SonarSource is a company focused on code quality and static analysis. This year, SonarSource, along with RIPS Technologies will be tweeting code challenges from real world vulnerabilities on their twitter @SonarSource.More information regarding this can be seen here: https://blog.sonarsource.com/code-security-advent-calendar-2020/.

This blog post will go through some of the solutions. Note: this might be wrong from the intended solution given by SonarSource

Challenge 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from django.contrib import auth, messages
from django.http import HttpResponseRedirect
from django.shortcuts import redirect, render
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, FormView, RedirectView


class RegisterView(CreateView):
    model = User
    form_class = RegistrationForm
    template_name = "register.html"
    success_url = "/"
    def post(self, request, *args, **kwargs):
        form = self.form_class(data=request.POST)
        if form.is_valid():
            user = form.save(commit=False)
            password = form.cleaned_data.get("password1")
            user.set_password(password)
            user.save()
        return redirect("login")
        
        
    def dispatch(self, request, *args, **kwargs):
        if self.request.user.is_authenticated:
            return HttpResponseRedirect(self.get_success_url())
        return super().dispatch(self.request, *args, **kwargs)
    def get_success_url(self):
        if "next" in self.request.GET and self.request.GET["next"] != "":
            return self.request.GET["next"]
        else:
            return self.success_url
            
            
    def get_form_class(self):
        return self.form_class

The issue here arises from the get_success_url function and an Open Redirect vulnerability.. This function checks for a parameter called next and makes a redirect to this parameter value. As such it is possible to djangoapplication?next=//attackersite.url. This redirect will happen after self.request.user.is_authenticated is successful.

Challenge 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.restservice;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.io.IOUtils;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
@RestController
public class RequestController {
    private static final Logger logger = null;
    private final AtomicLong counter = new AtomicLong();
    @RequestMapping(value = {"/api/adapter/{adapter}/activate/{b}"}, 
            method = RequestMethod.POST, produces = "application/json")
    public String activateAdapter(
            @PathVariable("adapter") String connName, 
            @PathVariable("b") Integer b) throws IOException {
        logger.info("activating adapter.");
        HttpPost post = new HttpPost("https://" + connName + "/v1/boot");
        String requestBody = "{\"activate\":" + Integer.toString(b) + "}";
        StringEntity requestEntity = new StringEntity(
            requestBody, 
            ContentType.APPLICATION_JSON
        );
        post.setEntity(requestEntity);
        try (CloseableHttpClient httpClient = HttpClients.createDefault();
             CloseableHttpResponse response = httpClient.execute(post)) {
            logger.info("response:" + response.getEntity());
            return EntityUtils.toString(response.getEntity());
        }
    }
}

The issue here arises from the adapter parameter coming from user input. This is given as part of the HttpPost constructor and a new object called postis made which has “https://” + connName + “/v1/boot”. This is then given to the httpClient.execute method which will make a request to the crafted url and return the response. This can be abused for an Server Side Request Forgery (SSRF) attack

Challenge 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php 
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
class LoginController extends AbstractController {
    private DOMDocument $doc;
    private $authFile = 'employees.xml';
    
    private function auth($userId, $passwd) {
        $this->doc->load($this->authFile);
        $xpath = new DOMXPath($this->doc);
        $filter = "[loginID=$userId and passwd='$passwd'][position()<=1]";
        $employee = $xpath->query("/employees/employee$filter");
        return ($employee->length == 0) ? false : true;
    }    
    public function index(Request $request) {
        $userId = (int)$request->request->get('userId');
        $password = $request->request->get('password');
        if ($request->request->get('submit') !== null) {
            try {
                if (!$this->auth($userId, $password)) {
                    return $this->json(['error' => "Wrong $userId."]);
                }
                else {
                    $this->loginCompleted(true);
                    $this->loadUserInformation($employee);
                }
            } catch (Exception $e) {
                return $this->json(['error' => "Login Failed."]);
            }
        }
        return $this->json(['error' => "Login Succeeded."]);
    }
}

The vulnerability here is an XPath Injection. Within function auth, the $userId and $passwd parameter is used to create an XPath query without any sanitization/validation: loginID=$userId and passwd=’$passwd’This query is used to check if the provided userid and password exists within employees.xml. This check is within the line return ($employee->length == 0) ? false : true;. This could be abused by a user and bypass this check by injecting something like ‘ OR ‘6’=’6 .

Challenge 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System.IO;
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc;
namespace core_api.Controllers
{
    public class DataDownloadController : Controller {
        public readonly string AvatarFolder = "images/avatars/";
        [HttpGet]
        public HttpResponseMessage GetAvatar(string image) {
            if (string.IsNullOrWhiteSpace(image) || image.Contains("/")) {
                return new HttpResponseMessage(HttpStatusCode.BadRequest) {
                    Content =  new StringContent("Valid avatar required")};    
            }
            string img = System.IO.Path.Combine(AvatarFolder, image);
            if (!img.Contains(AvatarFolder) || !System.IO.File.Exists(img)) { 
                return new HttpResponseMessage(HttpStatusCode.NotFound) {
                    Content =  new StringContent("Avatar not found")};
            }
            var fileInfo = new System.IO.FileInfo(img);
            var type = fileInfo.Extension;
            var c = new StreamContent(fileInfo.OpenRead());
            c.Headers.ContentType = new System.Net.Http.Headers.
                MediaTypeHeaderValue("image/" + type);
            return new HttpResponseMessage(HttpStatusCode.OK){Content = c};
        }
    }
}

In the above C# code, the source where tainted data is coming from the HttpGet method. This is then given to the GetAvatar as an argument. This function checks if the user value contains a / character and if it does, an error is returned to the user. If this check is not triggered, then the user provided value is combined with images/avatar path using the System.IO.Path.Combine method. A new constructor is created with the System.IO.FileInfo class which provides methods for creation, copying, deletion, moving, and opening of files. The File.OpenRead method is used to fetch the file in this path. On windows systems, the ..\ pattern or ../ pattern can be used to traverse directories. As such, the above code is vulnerable to Path Traversal.

Challenge 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from django.contrib import messages
from django.shortcuts import render, redirect
from django.contrib.sites.shortcuts import get_current_site
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from django.views.decorators.http import require_http_methods
from hashlib import sha1
from project.decorators import check_recaptcha
from project.forms import UserSignUpForm
from project.settings import config
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
from django.contrib.auth import get_user_model
User = get_user_model()
@check_recaptcha
@require_http_methods(["POST"])
def register(request):
    form = UserSignUpForm(request.POST)
    if form.is_valid() and request.recaptcha_is_valid:
        user = form.save(commit=False)
        user.is_active = False
        user.save()
        message = render_to_string('mail/activate.html', {
            'user': user,
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': sha1(force_bytes(user.pk)).hexdigest(),
        })
        message = Mail(
            from_email='noreply@' + get_current_site(request).domain,
            to_emails=request.POST.get('email'),
            subject='Your account activation email',
            html_content=message)
        response = SendGridAPIClient(config['SG_API_KEY']).send(message)
        messages.add_message(request, messages.SUCCESS, 'Verification email sent.')
    else:
        return render(request, 'account/register.html', {'form': form})
    return render(request, 'account/register.html', {'form': UserSignUpForm()})

In the above code, the SHA1 hashing algorithm is used without a salt to generate a token.: sha1(force_bytes(user.pk)).hexdigest(). As such, an attacker could generate a list of SHA1 hashes and try and brute force the password reset email token of a user. The vulnerability here is Insecure Token Generation.

Challenge 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package org.example;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.*;
public class IndexServlet extends HttpServlet {
    private String referer;
    private ExportIcalManager exportManager;
    private void exportIcal(HttpServletResponse res, String sessionId) 
            throws ServletException, IOException {
        res.addHeader("Access-Control-Allow-Origin", referer);
        res.setContentType("text/plain");
        ExportIcalManager exportManager = new ExportIcalManager(sessionId);
        String filePath = exportManager.exportIcalAgendaForSynchro();
        OutputStream os = res.getOutputStream();
        FileInputStream fs = new FileInputStream(filePath);
        int i;
        while (((i = fs.read()) != -1)) { os.write(i); }
        os.close();
    }
    protected void doPost(HttpServletRequest req, HttpServletResponse res) 
            throws ServletException, IOException {
        HttpSession session = req.getSession();
        referer = req.getParameter("referer");
        exportIcal(res, req.getRequestedSessionId());
    }
}

This vulnerability the above code is exposed to is Misconfigured Cross Origin Resource Sharing (Cors). The referer header from a user’s request is used to set the Access-Control-Allow-Origin header. An attacker could spoof this header to provide a wildcard *, or modify this through a redirect from an attacker site to allow cross origin communication.

Challenge 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
class Upload
{
    private $detect_mime = TRUE;
    private $type_regex = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/';
    public function do_upload($field = 'userfile') {
        $file = $_FILES[$field];
        $this->file_size = $file['size'];
        $this->_file_mime_type($file);
    }
    private function _file_mime_type($file) {
        if (function_exists('finfo_file')) {
            $finfo = @finfo_open(FILEINFO_MIME);
            $mime = @finfo_file($finfo, $file['tmp_name']);
            if (preg_match($this->type_regex, $mime, $match)) {
                return $this->file_type = $match[1];
            }
        }
        $cmd = 'file --brief --mime '.$file['name'].' 2>&1';
        exec($cmd, $mime, $status);
        if ($status === 0 && preg_match($this->type_regex, $mime, $match)) {
            return $this->file_type = $match[1];
        }
    }
}
$upload = new Upload();
$upload->do_upload();

The do_upload function takes a user provided file (i assume) and the file_mime_type function is called to open this file. The name of this file is taken by the $file[‘name’] object and is provided as part to the exec ($cmd = ‘file –brief –mime ‘.$file[‘name’].’ 2>&1’;) function. The vulnerability here is a Command Injection.

Challenge 8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Core31Demo.Controllers
{
    public class HomeController : Controller {        
        [HttpGet]
        public async Task<IActionResult> Logout(string logoutId) {
            ViewBag.Logout = "Please confirm logout &#8230;";
            if (User.Identity.IsAuthenticated == false) {
                return await Logout(new LogoutViewModel { LogoutId = logoutId });
            }
            return View("ConfirmLogout");
        }
        [HttpPost][ValidateAntiForgeryToken]
        public async Task<IActionResult> Logout(LogoutViewModel model) {
            await PerformSignOutAsync(model);
            ViewData["Logout"] = model.LogoutId;
            return View("Logout");
        }
        
        static async Task PerformSignOutAsync(LogoutViewModel model) {
            // sign out logic
            // throw new NotImplementedException();
        }
    }
    
    public class LogoutViewModel {
         public string LogoutId; 
    }
}


// Views/Home/Logout.cshtml

<div class="page-header">
    <h1>@Html.Raw(@ViewBag.Logout)</h1></div>
<div class="logout-back"><a asp-controller="Home" 
     asp-action="Login" asp-route-id="@ViewData["Logout"]">back</a>
</div>

A Cross-site Scripting (XSS) issue exists in the above code. User input is taken as the logoutId parameter ( through HttpGet). A new LogoutViewModel instance is created with the logoutId parameter: Logout(new LogoutViewModel { LogoutId = logoutId });. This is then given to the Task method which bind its to the @ViewBag.Logout model attribtue. The method Html.Raw() returns an IHtmlString with any HTML encoding. As such, XSS is possible.

Challenge 9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from __future__ import unicode_literals
import os
import shutil
import tempfile
import traceback
import zipfile
from django import forms
from django.http.response import HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView
from django.views.decorators.csrf import csrf_exempt
class AddonUploadView(FormView):
    form_class = forms.Form
    template_name = "package/addon/upload.jinja"
    def get_addon_path(self):
        filename = os.path.basename(self.request.GET.get("my_file"))
        tmp_token = self.request.GET.get('my_token')
        path = os.path.join(tempfile.gettempdir(), tmp_token, filename)
        if not os.path.isfile(path):
            raise ValueError("Error! File not found.")
        if hasattr(os, "geteuid") and os.stat(path).st_uid != os.geteuid():
            raise ValueError("Error! File not owned by current user.")
        return path
    @csrf_exempt
    def form_valid(self, form):
        try:
            installer.install_package(self.get_addon_path())
            response["success"] = True
        except Exception:
            os.unlink(self.get_addon_path())
            response["success"] = False
        return self.render_to_response(response)

Three issues are described in the above code:

  • Cross Site Request Forgery - @csrf_exempt in Django removes Cross Site Request Forgery protection allowing the request to be forged by an attacker.
  • Path Traversal - The path generated by the AddonUploadView function is user tainted.
1
2
3
        filename = os.path.basename(self.request.GET.get("my_file"))
        tmp_token = self.request.GET.get('my_token')
        path = os.path.join(tempfile.gettempdir(), tmp_token, filename)

This is then given to the os.stat(path) function which will execute the file provided by the user tainted path allowing execution of a user controlled file. This same also exists in os.unlink(self.get_addon_path()) where a path traversal can lead to arbritary file delete.

Challenge 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
@WebServlet(value="/unzip", name="ZipUtils")
class ZipUtils extends GenericServlet {
    private static final String BASE_DIR = "projects";
    @Override
    public void service(ServletRequest req, ServletResponse res) throws IOException {
        File zipFile = new File(BASE_DIR, req.getParameter("file"));
        if (zipFile.getCanonicalPath().startsWith(BASE_DIR)) {
            File indir = new File("/tmp/local/my_jars");
            unjar(zipFile, indir);
        }
    }
    private File[] unjar(File uploadFile, File inDir) throws IOException {
        String uploadFileName = inDir + File.separator + uploadFile.getName();
        ZipFile uploadZipFile = new ZipFile(uploadFile);
        Set<File> files = new HashSet<File>();
        Enumeration entries = uploadZipFile.entries();
        // unpack uploaded zip file
        while (entries.hasMoreElements()) {
            ZipEntry entry = (ZipEntry) entries.nextElement();
            File fe = new File(uploadFileName, entry.getName());
            if (entry.isDirectory()) {
                fe.mkdirs();
            } else {
                if (fe.getParentFile() != null 
                && !fe.getParentFile().exists()) {
                    fe.getParentFile().mkdirs();
                }
                files.add(fe);
                IOUtils.copy(uploadZipFile.getInputStream(entry), 
                    new BufferedOutputStream(new FileOutputStream(fe)));
            }
        }
        uploadZipFile.close();
        return files.toArray(new File[files.size()]);
    }
}

The above code is vulnerable to Zip Path Traversal. User input (source) is entering via uploadZipFile.entries() method (Map.Entry). The code iterates through this Map and the getInputStream method is used to fetch the file and IOUtils.copy is used to copy bytes from an InputStream to chars on a Writer.

As such, you can create a ZIP archive containing an archive path such as ......\newfile and this file will be written outside the destination directory. This could be abused to overwrite files within the system.

Challenge 11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
use Illuminate\Routing\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Request;
class Authenticate{
    private function getEmail($email_field, $user_data) {
        foreach($email_field as $field) {
            if (isset($user_data[0][$field][0])) {
                return $user_data[0][$field][0];
            }
        }
        return NULL;
    }
    public function findUsername() {
        $envvar = $this->settings['fields']['envvar'];
        $ldapdn = Config::read('WebApp.ldapDN');
        $ldapSearchFilter = Config::read('WebApp.ldapSearchFilter');
        $ldapEmailField = Config::read('WebApp.ldapEmailField');
        $ldapconn = ldap_connect(Config::read('WebApp.ldapServer')) 
            or die('LDAP server connection failed');
        if (!($ldapbind = ldap_bind($ldapconn))) {
            die("LDAP bind failed");
        }
        if (!empty($ldapSearchFilter)) {
            $filter = '(&' . $ldapSearchFilter . '(' .
            Config::read('WebApp.ldapSearchAttribut') . '=' .
            Request::input($envvar) . '))';
        }
        $getLdapUserInfo = Config::read('WebApp.ldapFilter');
        $result = ldap_search($ldapconn, $ldapdn, $filter, $getLdapUserInfo)
            or die("LDAP Error: " . ldap_error($ldapconn));
        $ldapUserData = ldap_get_entries($ldapconn, $result);
        if (!isset($ldapEmailField) && isset($ldapUserData[0]['mail'][0])) {
            $username = $ldapUserData[0]['mail'][0];
        } else if (isset($ldapEmailField)) {
            $username = $this->getEmail($ldapEmailField, $ldapUserData);
        } else {
            die("User not found in LDAP");
        }
        ldap_close($ldapconn);
        return $username;
    }
}

User concentated data is inserted to the ldap_search function resulting in LDAP Injection: ldap_search($ldapconn, $ldapdn, $filter, $getLdapUserInfo) An ldap query is created with ldapSearchFilter and $envvar. $envvar can be user controlled data, and as such LDAP Injection is possible.

Challenge 12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
namespace core_api.Controllers
{
    public class BlogPost {
        public DateTime ReleaseDate { get; set; }
        public string Content { get; set; }
    }
    public class BlogController : Controller
    {
        ArrayList Posts = new ArrayList();
        string Keyword = "[highlight]";
        
        public void init() {
            Posts.Add(new BlogPost{ReleaseDate = new DateTime(2009, 8, 1, 0, 0, 0), Content="[highlight]"});
        }
       
        [Route("api/search")]
        [HttpGet]
        public ArrayList search(string search, string since) {
            DateTime.TryParseExact(since, "MM-dd-yy", null,
                DateTimeStyles.None, out var parsedDate);
            var blogposts = from BlogPost blog in Posts
                where DateTime.Compare(blog.ReleaseDate, parsedDate) > 0
                select blog.Content;
            ArrayList result = new ArrayList();
            foreach (var content in blogposts) {
                String tmp = content.Replace(Keyword, search);
                Regex rx = new Regex(search);
                Match match = rx.Match(tmp);
                if(match.Success) {
                    result.Add(match.Value);
                }
            }
            return result;
        }
    }
}

A Reject Injection/ReDoS vulnerability exists in the above code. The search argument is provided by user input. This is then provided to the Regex constructor and the user provided input is used for the regex match. An attacker could provide a large regex which could lead to backtracking when eveluating with Regex.

Challenge 13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
import requests
import errno
from django.conf import settings
class Sitemap():
    domain = "sonarsource.com"
    def __init__(self, filename="sitemap.xml"):
	self.url = "http://" + self.domain
        if not self.url.endswith('/'):
            self.url += '/'
        self.url += filename
        self.destination = os.path.join(settings.STATIC_ROOT, filename)
	
    def fetch(self, dataset=None):
        headers = {'User-Agent': 'Mozilla/5.0 Chrome/50.2661 Safari/537.36'}
        try:
            response = requests.get(self.url, timeout=30, headers=headers)
        except requests.exceptions.SSLError:
            response = requests.get(self.url, verify=False, headers=headers)
        finally:
            pass
        with open(self.destination, 'wb') as f:
            f.write(response.content)

Two issues exist in the above code:

  • http:// protocol URI is used as the URL that will be used by requests.get. Usage of Plain text protocols** are considered bad practice.
  • The verify=False option is set for requests.get. This is disables server certificate validation and is considered to be Insecure Configuration.

Challenge 14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
public class IndexServlet {
    private final ServletContext context;
    private final String templateFile = "/org/java/repository.xml";
        
    public IndexServlet(ServletContext context) {
        this.context = context;
    }
    
    public void installRepository(HttpServletRequest req)
            throws ServletException, IOException {
        String mode = req.getParameter("mode");
        String repHome = req.getParameter("repository_home");
        if (repHome != null && mode != null && "new".equals(mode)) {
            installConfig(new File(repHome));
        }
    }
    private void installConfig(File dest) throws IOException {
        InputStream in = context.getResourceAsStream(templateFile);
        OutputStream out = new FileOutputStream(dest);
        byte[] buffer = new byte[8192]; int read;
        while ((read = in.read(buffer)) >= 0) {
            out.write(buffer, 0, read);
        }
        in.close();
        out.close();
    }
}

Tainted source is flowing from req.getParameter(“repository_home”);, this is the provided to the new File constructor and flows to the installConfig method. The user input then flows to the FileOutputStream(dest) method which creates a new file in the give file path, resulting in Arbritary File Write.

Challenge 15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
if ('restore' == $_GET['action']) {
    $upload = $_FILES['filename'];
    $upload_tmp = $_FILES['filename']['tmp_name'];
    $upload_name = $_FILES['filename']['name'];
    $upload_error = $_FILES['filename']['error'];
    if ($upload_error > 0) {
        switch ($upload_error) {
            case UPLOAD_ERR_INI_SIZE:
                break;
            default:
                echo sprintf("Error %s", $upload_error);
        }
    }
    if (!$upload_name && isset($_POST['file'])) {
    $upload_name = filter_input(INPUT_POST,'file',FILTER_SANITIZE_STRING);
    } else {
        $ret_val = do_upload($upload_tmp, $upload_name);
    }
    echo '<p><b>restore from ' . $upload_name . '</b>';
}

Multiple issues exist in the above code:

  • An Insufficient Validation issue exists where a file with any extension can be uploaded
  • Two Cross-site Scripting (XSS) issues exist. One within the sprintf sink where $upload_error is reflected back a a user. Another XSS exists where $upload_name is reflected back to a user in the echo statement.

Challenge 16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.AspNetCore.Mvc;
[Serializable]
public class ExchangeData {
}
namespace core_api.Controllers {
    public class ShareDataController : Controller {
        [Route("import/exchange")] 
        [HttpPost]
        public string ImportExchangeData(string content) {
            var xmlDoc = new XmlDocument { XmlResolver = null };
            xmlDoc.LoadXml(content); 
            var rootItem = (XmlElement)xmlDoc.SelectSingleNode("root");
            var dataType = Type.GetType(rootItem.GetAttribute("data"));
            var reader = new StringReader(rootItem.InnerXml);
            ExchangeData exchange = Import(dataType, reader);
            return exchange.ToString();
        }
        private static ExchangeData Import(Type t, StringReader r) {
            XmlSerializer serializer = new XmlSerializer(t);
            XmlTextReader textReader = new XmlTextReader(r);
            ExchangeData data = (ExchangeData)serializer.Deserialize(textReader);
            return data;
        }
    }
}

An Unsafe Deserialization vulnerability exists in the above code. XmlSerializer is used to deserialize user object provided by the content argument. For XmlSerializer serializer, the expected type should not come from user-controlled input. In the above code, the expected data type is coming from rootItem.GetAttribute(“data”).

Challenge 17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import copy
import io
from http import HTTPStatus
from zipfile import ZipFile
from flask import Blueprint, current_app, jsonify, request, send_file
@FIXTURE_BLUEPRINT.route('/api/export/<business_id>', methods=['GET'])
def get(business_id, table=None):
    con = current_app.config.get('DB_CONNECTION', None)
    if not con:
        current_app.logger.error('Database connection failure')
        return jsonify({'message': 'Database connection error'})
    cur = con.cursor()
    bid = _get_business_id(cur=cur, business_id=business_id)
    if not bid:
        current_app.logger.error(f'{business_id} not found')
        return jsonify({'message': f'Could not find {business_id}.'})
    try:
        tmp_file = _create_export(cur=cur, table=table, bid=bid)
        if not tmp_file:
            return jsonify({'message': f'Failed to create export for {bid}'})
        current_app.logger.error(f'DELETE: {tmp_file}')
        return send_file(attachment_filename=tmp_file, as_attachment=True)
    except Exception as err:
        current_app.logger.error(f'Failed to export')
        con.reset()
        return jsonify({'message': 'Failed to export data.'})

A Log Injection vulnerability exists where data source coming from business_id is validated and if it doesn’t exist, this is logged into the current_app.logger.error method directly. An attacker could inject CRLF characters (%0D%0A) and adding additional log entries.

Challenge 18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.example;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
public class IndexServlet extends HttpServlet {
    private String extractContent() throws IOException, JDOMException {
        File uploadFile = new File("/users/upload/document.odt");
        InputStream in = new FileInputStream(uploadFile);
        final ZipInputStream zis = new ZipInputStream(in);
        ZipEntry entry;
        List<Content> content = null;
        while ((entry = zis.getNextEntry()) != null) {
            if (entry.getName().equals("content.xml")) {
                final SAXBuilder sax = new org.jdom2.input.SAXBuilder();
                Document doc = sax.build(zis);
                content = doc.getContent();
                StringBuilder sb = new StringBuilder();
                if (content != null) {
                    for (Content item : content) {
                        sb.append(item.getValue());
                    }
                }
                zis.close();
                return sb.toString();
            }
        }
        return null;
    }
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        try {
            extractContent();
        }
        catch(Exception e) {
            return;
        }
    }
}

org.jdom2.input.SAXBuilder is used to parse an .odt file. An .odt file contains XML files will be parsed by SAXBuilder, content.xml in this example. In the above example, SAXBuilder is set without builder.setFeature(“http://apache.org/xml/features/disallow-doctype-decl”,true); which provides an attack vector for XML External Entity (XXE). An attacker could modify the content.xml to have external entities.

Challenge 19

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Http\RedirectResponse;
class LessonsController6 extends ApiController {
    public function load(): RedirectResponse {
        if (!empty(Request::cookie('site'))) {
            $site_id = Request::cookie('site');
        } else if (!empty(Request::getHost())) {
            $site_id = Request::getHost();
        } else {
            $site_id = 'default';
        }
        if (empty($site_id) || preg_match('/[^A-Za-z0-9.-_]/', $site_id)) {
            abort(403, 'Invalid ID ' . htmlspecialchars($site_id, ENT_NOQUOTES));
        }
        require_once "sites/$site_id.php";
        if ($config == 1) {
            return redirect()->route('login', ['site' => $site_id]);
        } else {
            return redirect()->route('setup', ['site' => $site_id]);
        }
    }
}

A Local File Inclusion exists in the above code. It is possible to set an arbritary path through a host header (Request::getHost() or a Cookie Request::cookie(‘site’), The regex used by the code ([^A-Za-z0-9.-_]) does not validate ../../ and this $siteid is provided to require_once “sites/$site_id.php”;. The .php extension can be bypassed using a null byte (%00).

Challenge 20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
namespace core_api.Controllers
{
    public class ApiController : Controller
    {
        private readonly string publicKey;        
	private readonly string authTokenIssuer;
        
        public ApiController(string publicKey, string authTokenIssuer)     
        {
            this.publicKey = publicKey;
            this.authTokenIssuer = authTokenIssuer;
        }
        
        private String ExportPublicKey(RSACryptoServiceProvider rsa)
        {
            throw new NotImplementedException();
        }
        private RSACryptoServiceProvider ImportKeyParameters(string publicKey)
        {
            throw new NotImplementedException();
        }
	    
        public JwtSecurityToken ValidateToken(string token) {           
            byte[] keyBytes = Convert.FromBase64String(publicKey);
            var keyParams = (RsaKeyParameters)PublicKeyFactory.CreateKey(keyBytes);
            var rsaParams = new RSAParameters {
                Modulus = keyParams.Modulus.ToByteArrayUnsigned(),
                Exponent = keyParams.Exponent.ToByteArrayUnsigned() 
            };
            var rsa = new RSACryptoServiceProvider();
            rsa.ImportParameters(rsaParams);
            rsa.KeySize = 4096;
            var validationParameters = new TokenValidationParameters {
                RequireExpirationTime = true,
                RequireSignedTokens = true,
                ValidIssuer = authTokenIssuer,
                ValidateIssuer = true,
                ValidateLifetime = true,
                IssuerSigningKey = new RsaSecurityKey(rsa)
            };
            var handler = new JwtSecurityTokenHandler();
            handler.ValidateToken(token, validationParameters, out SecurityToken validatedSecurityToken);
            var validatedJwt = validatedSecurityToken as JwtSecurityToken;
            return validatedJwt;
        }       
    }
}

The above code is an example of Insecure Cryptography. RSACryptoServiceProvider constructor by default sets the key size to be 1024. The rsa.KeySize = 4096; assingment does not change the keysize according to the SonarSource expected solution.

Challenge 21

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import os
import tempfile
'''
Open the pdf reader on windows for the report file
'''
def open_report(report_class, _system="Windows", *args, **kwargs):
    rv = PrintReportEvent.emit(report_class, *args, **kwargs)
    if rv:
        return rv
    filters = kwargs.pop('filters', None)
    if filters:
        kwargs = describe_search_filters_for_reports(filters, **kwargs)
    tmp = tempfile.mktemp(suffix='.pdf', prefix='stoqlib-reporting')
    report = report_class(tmp, *args, **kwargs)
    report.filename = tmp
    if _system == "Windows":
        report.save()
        log.info("Executing PDF reader with %r" % (report.filename, ))
        os.startfile(report.filename)
        return
    if isinstance(report, HTMLReport):
        op = PrintOperationWEasyPrint(report)
        op.set_threaded()
    else:
        op = PrintOperationPoppler(report)
    rv = op.run()
    return rv

The issue here arises from the tempfile.mktemp function being used which allows for potential Race Condition. An attacker could create a file of his choice before the mktemp is triggered by an user. The following stackoverflow explains this vulnerability: Stackoverflow - using-python-tempfiles-permanently

Challenge 22

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.restservice;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.Criterion;
import org.hibernate.type.StringType;
@Controller
@RequestMapping("/nodeList.htm")
public class NodeListController {
    public NodeListModel createNodeList(NodeListCommand command) {
        NodeCriteria criteria = new NodeCriteria(Node.class, "node");
        addNodeCriteria(criteria, command.getNodeParm(), command.getNodeParmValue());
        return createModel(command);
    }
    @RequestMapping(method={ RequestMethod.GET, RequestMethod.POST })
    public ModelAndView handle(@ModelAttribute("command") NodeListCommand command) {
        NodeListModel model = createNodeList(command);
        ModelAndView modelAndView = new ModelAndView("nodeList", "model", model);
        return modelAndView.addObject("command", command);
    }
    private static void addNodeCriteria(NodeCriteria criteria,
            String nodeParm, String nodeParmValue) {
        final String nodeParameterName = ("snmp" + nodeParm).toLowerCase();    
        criteria.add(Restrictions.sqlRestriction(nodeParameterName + " = ?)", 
            nodeParmValue, new StringType()));       
        criteria.createAlias(nodeParm, nodeParameterName);    
    }
    
    private NodeListModel createModel(NodeListCommand command) {
        return new NodeListModel();
    }
}

A SQL Injection exists in the above code. Tainted data is flowing from source @ModelAttribute("command") NodeListCommand command. The command argument is then sent to the createNodeList method. The command then flows to the addNodeCriteria method. The nodeParm argument from the addNodeCriteria method is then concentated to the nodeParameterName variable: final String nodeParameterName = ("snmp" + nodeParm).toLowerCase();. This then added to the createAlias HQL sink via criteria.addresulting in SQL injection.

Challenge 23

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php 
class email_output_html {
    protected function express($expression) {
        $expression = preg_replace(
            array('/env:([a-z0-9_]+)/i',
                '/config:([a-z0-9_]+)(:(\S+))?/i',
            ),
            array("(isset(\$this->env['\\1']) ? \$this->env['\\1'] : null)",
                "\$this->config->get('\\1', '\\3')",
            ),
            $expression
        );
        return eval("return ($expression);");
    }
    protected function parse_template() {
        $attributes  = html::parse_attrib_string($_POST['_mail_body']);
        foreach($attributes as $attrib) {
            if (!empty($attrib['express'])) {
                $attrib['c'] = $this->express($attrib['express']);
            }
            if (!empty($attrib['name']) || !empty($attrib['command'])) {
                $attrib['c'] = $this->button($attrib);
            }
        }
    }
}
class html {
    public static function parse_attrib_string($str) {	
        $attrib = array();
        preg_match_all('/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui', $str, $regs, PREG_SET_ORDER);
		
        if ($regs) {
            foreach ($regs as $attr) {
                $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
            }
        }
        return $attrib;
    }
}

A Code Injection exists due to the usage of eval. User input is flowing from the parse_template function to the express function as follows: express($attrib['express'] I wasn’t able to find a bypass for the regex used within the parse_template function; the SonarSource expected solution states that the following payload can be used: /config:sonar:'.phpinfo().’

Challenge 24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Diagnostics;
using System.Reflection;
namespace core_api
{
    public partial class Form1 : Form
    {
        const int MAX_PATH = 10;
        public Form1()
        {
            InitializeComponent();
        }
        private void btnInstallPackage_Click(object sender, EventArgs e) {
            InstallPackage(txtPackage.Text, CurrentProject.ProjectDirectory);
        }
        public static void InstallPackage(string packageId, string workingDir) {
            string dir = Path.Combine(workingDir, "nuget");
            dir = Path.Combine(dir, packageId).Substring(0, MAX_PATH);
            Directory.CreateDirectory(dir);
            Process nuget = new Process();
            nuget.StartInfo.FileName = Path.Combine(Tools.GetPath(), "nuget");
            nuget.StartInfo.Arguments = "install "+packageId+" -NonInteractive";
            nuget.StartInfo.CreateNoWindow = true;
            nuget.StartInfo.RedirectStandardOutput = true;
            nuget.StartInfo.RedirectStandardError = true;
            nuget.StartInfo.WorkingDirectory = dir;
            nuget.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8;
            nuget.StartInfo.UseShellExecute = false;
            nuget.Start();
        }
    }
    class Tools
    {
        public static string GetPath()
        {
            return "D:\\test\\VisualNuget\\nuget";
        }
    }
    class CurrentProject
    {
        public static string ProjectDirectory = "D:\\test\\VisualNuget";
    }
}

The issue here arisies from the InstallPackage method taking a packageId parameter. This parameter is concentated into the arguments :nuget.StartInfo.Arguments = “install “+packageId+” -NonInteractive”;and is ran as part of the nuget command. It is not possible to conduct a Command Injection attack due to user input only ending up in nuget.StartInfo.Arguments but Argument Injection is possible and run the command with additional arguments that the binary wasn’t expecting.

This post is licensed under CC BY 4.0 by the author.