PHP2Shell 소식을 접하고 개인적으로 운영하는 PHP 서비스들의 실행 환경을 정리했다. 보안 패치가 들어간 지원 버전으로 옮기면서 오래된 코드의 호환성을 고치고, 실제 웹 요청이 새 환경으로 연결되는지도 확인했다. 보안 업데이트와 레거시 유지보수가 함께 따라온 작업이었다.

대응 과정에서 손이 많이 간 부분은 오래된 서비스가 새 환경에서도 제 역할을 하는지 확인하는 일이었다. 내부 주소나 서버 구성도는 빼고, 비슷한 서비스를 운영할 때 참고할 만한 확인 과정을 남긴다.

PHP2Shell은 어떤 문제인가

PHP2Shell은 CVE-2026-109618과 CVE-2026-109619를 함께 부르는 이름이다. 인증되지 않은 원격 공격자가 코드를 실행할 수 있는 PHP 핵심부의 취약점 두 건으로, 공식 발표는 2026년 10월 21일에 나왔다. 싱가포르 사이버보안청은 두 건의 심각도를 CVSS v4.0 기준 9.2로 안내했다. 보안기관 공지

하나는 메모리 부족 오류를 처리하다가 내부 상태가 불완전하게 남는 문제이고, 다른 하나는 요청이 끝나 해제된 메모리를 다음 요청에서 다시 참조할 수 있는 문제다. 특정 게시판의 입력값 검증 실수에만 한정된 이야기가 아니다. PHP 자체가 요청을 처리하는 과정에 문제가 있다는 점 때문에 실행 환경부터 확인해야 했다. 메모리 부족 처리 취약점 · 요청 사이에 남는 상태의 취약점

PHP 공식 공지는 PHP-FPM을 포함한 여러 웹 실행 방식을 대상으로 명시한다. mod_php에서 위험이 특히 크다고 해서 다른 방식은 확인하지 않아도 된다는 뜻은 아니다. 이 두 취약점의 수정 버전은 PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12다. 오래된 버전은 배포판이나 장기 지원 업체의 백포트 여부까지 확인해야 하므로, 버전 계열 이름만 보고 안전하다고 판단할 수 없다. PHP 공식 보안 공지

먼저 어느 PHP가 웹 요청을 처리하는지 봤다

한 서버에서 오래 운영한 서비스라고 해서 전부 같은 PHP로 동작하는 것은 아니었다. 기본 설정을 따라가는 사이트도 있고, 호환성 때문에 별도 버전을 지정해 둔 사이트도 있었다. 기본 PHP만 바꾸면 일부 서비스는 예전 실행 환경에 그대로 남을 수 있는 구조였다.

그래서 실제로 활성화된 웹서버 설정, 사이트별 처리 규칙, 실행 중인 PHP 프로세스를 함께 확인했다. 명령행에서 출력되는 버전과 브라우저 요청을 처리하는 버전은 따로 대조했다. 설치 파일이 남아 있는 것과 웹 요청에 사용되는 것도 구분했다.

이번 작업의 완료 기준은 대상 서비스가 패치된 지원 버전으로 연결되고, 구버전으로 웹 요청을 보내는 설정이나 실행 중인 구버전 처리기가 남지 않는 것이었다. 새 PHP가 설치됐다는 사실만으로 전환을 끝내지는 않았다.

원본을 기준으로 호환성을 비교했다

기존 소스와 설정을 먼저 보존하고 별도 시험 환경에서 실행했다. 처음부터 여러 부분을 한꺼번에 고치면 문제가 사라져도 무엇 때문인지 알기 어렵다. 우선 원본의 동작을 확인하고, 새 PHP에서 달라지는 부분을 좁혀 나갔다.

오래된 서비스에서는 제거된 문법이나 함수, 더 엄격해진 함수 호출과 타입 처리처럼 실행 환경 변화에 걸리는 부분이 나왔다. 실제로 필요한 호환성 수정을 적용했고, 기존 코드가 지키던 인증과 데이터 처리 동작도 함께 살폈다.

로그인, 글 편집, 첨부파일처럼 데이터를 쓰는 기능은 복제한 시험 환경에서 확인했다. 시험을 위해 운영 계정의 비밀번호를 바꾸거나 실제 게시물을 덮어쓰는 방식은 쓰지 않았다. 시험 환경의 DB 동작 조건도 운영과 맞춰, 환경 차이로 생긴 오류를 PHP 호환성 문제로 잘못 분류하지 않도록 했다.

이전부터 실패하던 경로와 새 버전에서 생긴 오류도 따로 비교했다. 이번 전환 때문에 생긴 문제는 수정했고, 별도로 손본 기존 오류는 그 사실을 구분해 기록했다. 오래된 서비스의 모든 오류를 업데이트 탓으로 돌리면 원인도 수정 범위도 흐려진다.

응답이 왔다는 것만으로는 부족했다

HTTP 200이 돌아와도 본문이 비어 있을 수 있다. JSON이어야 하는 응답에 다른 내용이 들어가도 상태 코드만 보면 놓친다. 실제로 데이터를 받아 쓰는 기능은 본문을 파싱하고 기대한 구조가 있는지 확인해야 했다.

반대로 인증되지 않은 요청에 접근을 거절하거나 잘못된 입력에 오류를 반환하는 것은 정상 동작일 수 있다. 성공 코드의 개수를 세기보다 원래 그 요청에서 기대한 결과와 맞는지를 봤다.

확인한 것그 결과로 알 수 있는 범위
새 PHP의 설치와 구문 검사실행 파일과 소스 문법의 준비 상태
실제 웹 요청의 실행 환경사이트가 새 PHP로 연결됐는지
응답 본문과 기대한 데이터 구조검사한 API가 필요한 결과를 돌려주는지
격리 환경의 로그인·편집·업로드시험한 쓰기 흐름이 새 환경에서도 동작하는지

API 데이터가 나온 결과, 실시간 연결이 성립한 결과, 브라우저에서 기능을 끝까지 사용한 결과도 각각 구분했다. 어느 하나가 통과했다고 전체 기능을 확인한 것으로 묶지는 않았다.

후속 공지는 적용 조건을 다시 대조했다

10월 23일에는 세션 업로드 진행률 기능에 관한 후속 공지가 나왔다. 초기 세션 직렬화 설정과 애플리케이션 실행 중의 설정 변경이 특정하게 조합돼야 하는 문제였다. PHP 세션 업로드 진행률 후속 공지

이 부분은 현재 PHP 설정과 배포된 소스를 다시 읽고, 실행 중인 PHP에 보내는 비공개 진단 요청으로 확인했다. 점검한 운영·시험 환경에서는 공지의 발생 조건에 해당하지 않았다. 따라서 이 후속 문제 때문에 추가 설정을 바꾸거나 서비스를 재시작하지 않았다.

일반 파일 업로드와 PHP가 세션에 업로드 진행률을 기록하는 기능도 나눠서 봤다. 후속 점검에서는 작은 파일을 이용해 PHP의 기본 업로드 처리를 확인했으며, 이것을 운영 게시물에 첨부파일을 쓰는 전체 시험으로 표현하지는 않았다. 임시 진단 파일은 확인 후 제거했다.

이번 대응을 마친 기준

대상 서비스들을 보안 패치가 포함된 지원 PHP 환경으로 전환했고, 기존 구버전이 웹 요청을 계속 처리하지 않는 것까지 확인했다. 호환성 수정과 시험을 거쳐 운영에 반영했으며, 이후의 후속 공지도 현재 구성에 대조했다. 웹서버 보안 업데이트도 별도로 진행했다.

이 기록은 PHP2Shell 대응과 그에 따른 서비스 전환에 관한 것이다. 과거 침해가 없었다는 조사 결과나 모든 애플리케이션의 보안 감사를 뜻하지는 않는다. 운영 서버에 공격 코드를 실행해 확인하는 방식도 사용하지 않았다.

오래된 서비스는 실행 환경을 바꾸는 순간 코드의 호환성 문제가 한꺼번에 드러날 수 있다. 이번에는 원본과 바뀐 환경을 비교하고, 데이터를 쓰는 시험은 분리하고, 실제 웹 요청이 새 환경을 사용하는 데까지 확인했다. 변경 내역과 되돌릴 지점도 별도로 남겨 두었다.

참고 자료

2026/10/25 03:11 2026/10/25 03:11
AES랑 DES를 풀이할때 유용하게 사용되는 Crypto 라이브러리를 사용하려는데 pip으로 설치했는데도 잘 안되는 개같은 경우가 발생한다.
이때 침착하게 다음 스탭을 따라보자.

pip install crytpto
pip install pycrypto


이렇게 설치하였나?
그럼 당신은 실패하였다........

crypto 패키지를 깔게 되면 cipher (lower case)가 생성되고 참조가 안된다 ㅡㅡ

pip uninstall crypto
pip uninstall pycrypto

pip install pycrypto


반드시 하나만! 해주자. 내생각에는 pip 이슈인것같다.
2016/04/09 00:23 2016/04/09 00:23
MySQL 포트는 아주 공개적으로 열려있다. 3306이며 포트를 바꿔도 공격에 자연스럽게 노출되게 된다. 오픈소스의 숙명이라고 할 수 있겠다.

서버 세팅한지 얼마 되지도 않았는데 어찌나 많은 불나방들이 달려드는지 골머리를 앓고 있던차에,
이녀석들을 제거하는 일을 자동화를 해야겠다고 결심했다.

그래서 fail2ban을 깔아서 처리해보려고 했으나 이게 왠일, fail2ban은 정말 SSH(22)만 검사하는 프로그램이었던것이다.
찾아보니 그냥 검사나 제때하고 포트바꿔라 이런소리나 하고 있고...
간단하게 코딩해보기로 했다. 크게 어려울것 같지 않았다. (부작용은 아직 모르겠다)

mysql.log를 남기는것은 성능에 마이너스이므로 에러로그와 슬로우쿼리만 남기고 있다.
두개 로그는 파일 크기가 그렇게 크지 않기때문에 로드하는것에도 부하가 없을 것이라고 생각(근거 없는 믿음)한다

#!/usr/bin/env python
# -*- coding: utf8 -*-

import re
from subprocess import call

path = "/var/log/mysql/"
log = "error.log"

logContents = ""

excludeText = "etc ips............................"

with open("{}{}".format(path,log)) as f:
    logContents = f.read()

extractedIP = {}
for ip in re.findall( r'\'?\'@\'[0-9]+(?:\.[0-9]+){3}\'', logContents):
    ip = ip.replace('@','').replace('\'','')
    try:
        if type(extractedIP[ip]):
            extractedIP[ip] = extractedIP[ip] + 1
    except:
        extractedIP[ip] = 1

for index in extractedIP:
    if extractedIP[index] > 2 and index not in excludeText:
        call("iptables -A INPUT -s {} -j DROP".format(index), shell=True)

f = open("{}{}".format(path,log),"w")
f.write('')
f.close()

print "Done"


내가 로그를 살펴보아하니 공격은 거의 5분안에 이뤄지더라. [아래 로그 참조]
2016-02-07T18:46:47.070457Z 519468 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:47.330309Z 519469 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:47.521742Z 519470 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:47.670816Z 519471 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:47.796906Z 519472 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:47.915819Z 519473 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:48.037179Z 519475 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:48.146458Z 519476 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:48.253486Z 519477 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:48.355799Z 519478 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:48.470777Z 519480 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:48.670311Z 519481 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:48.891320Z 519482 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:49.034734Z 519483 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:49.167179Z 519486 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:49.361079Z 519487 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:49.555965Z 519489 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)
2016-02-07T18:46:49.800100Z 519490 [Note] Access denied for user 'root'@'180.97.215.141' (using password: YES)


1분에 약 적게는 3건 많게는 5건으로 시도하기때문에 3분마다 cron을 돌게 하고,
거절 된 아이피가 3건이상일 경우 해킹이라고 생각해 iptables를 이용해 블럭 하도록 처리했다.

단점이라면, 3분안에 공격이 연속되지 않으면 로그를 초기화하기때문에 잡아낼 수 없다는 단점이 있다.
즉시 처리 하기 위해 3분이라는 짧은 텀을 주었으나 10분정도를 줘도 무방할 것 같다.

심플한 코드인데 벌써 한 50개정도의 아이피를 블럭한것같다.
이 코드는 gist에서 버전 관리 될 예정이다.
2016/02/10 11:37 2016/02/10 11:37
우선 실시간으로 공격당해서 너무 어이가 없다.
왜냐면, 너무 당연한건데 처리하지 않은 나한테 어이가 없는게 젤 큰것 같다.

우선, 어제 노드 스크립트를 좀 수정하고 redis 키를 갱신하도록 코드를 짰는데, 낮에 갑자기 뜬금없이 모든 키들이 증발하기 시작했다. 나는 코드에서 이슈가 나는줄 알고 얼른 복구해봤지만 그대로 날아가긴 마찬가지..
답답한마음에 redis 키를 살펴보던 와중에 정말 재수가 좋았던건지 이걸 발견하게 된다.

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcuHEVMRqY/Co/RJ5o5RTZmpl6sZ7U6w39WAvM7Scl7nGvr5mS4MRRIDaoAZpw7sPjmBHz2HwvAPYGCekcIVk8Xzc3p31v79fWeLXXyxts0jFZ8YZhYMZiugOgCKvRIs63DFf1gFoM/OHUyDHosi8E6BOi7ANqupScN8cIxDGsXMFr4EbQn4DoFeRTKLg5fHL9qGamaXXZRECkWHmjFYUZGjgeAiSYdZR49X36jQ6nuFBM18cEZe5ZkxbbtubnbAOMrB52tQX4RrOqmuWVE/Z0uCOBlbbG+9sKyY9wyp/aHLnRiyC8GBvbrZqQmyn9Yu1zBp3tY8Tt6DWmo6BLZV4/ crack@redis.io


RSA 키인데, 키 이름은 crackit 이었다. 뭔가 깨림칙해서 구글링해보니...

http://antirez.com/news/96

이 링크부터 시작이다.
결론은 대부분의 redis 사용자들이 bind를 0.0.0.0 이나 로컬인 127..... 로 하기때문에, 텔넷을 붙여보면 쉽게 돌파할 수 있다는 내용이었다. 그거에 라이브로 털리고 있고 -_-

단순한 장난같아보였는데, 키는 키대로 다 날아가고 있고.. 특히 나는 세션을 레디스로 관리하기떄문에 사용자 세션이 정말 승천하는 중이었다. bind 세팅을 바꿔보니 제대로 붙질 못하고, 호스팅 업체에서 제공하는 파이어월을 세팅하니 노드가 못붙는다... 왠진 모르겠다.

결국 아이피테이블로 모든 접근을 제한하고, 웹서버에서 요청하는것만 붙을 수 있도록 정책을 수정했다.

iptables -A INPUT -p tcp -s IP  --dport PORT -j ACCEPT;
iptables -A INPUT -p tcp --dport PORT -j DROP
iptables -nvL


redis자체에서도 AUTH를 해줄 수 있으니,
소스코드를 수정해서 보안적으로 접근할 수 있도록 해야할 것이다. 당연히 오픈소스인 MySQL 도 그렇고...
보이는것 외에 보이지 않는것에도 신경을 제대로 써야겠단 생각이 든다
2015/11/10 16:13 2015/11/10 16:13
Assignment #3
주어진 평문, 암호문을 이용하여 사용된 두개의 키를 알아내는 과제

1. 암호문은 DES(ECB), AES-128(CBC)를 이용하여 암호화됨
2. 계산시간의 축약을 위해, password list가 주어진다. 리스트에는 md5 hash 값과, word가 한라인에 공백으로 구분되어 기재됨
2-1. 리스트는 약 18만 5천여개의 hash로 이루어져있다.
3. DES CBC이므로 8바이트 key를 사용해 암호화를 해야하므로, 주어진 hash에서 가장 앞글자 8자리를 사용할것
4. AES-128은 주어진 hash를 모두 사용해 암호화할것




외국사람들이 내 블로그 볼일이 있겠나 싶어 그냥 한글로 정리한다.

처음에 이 과제를 받고 어떻게 해야하나 생각을 해봤다.
평문과 암호화된 텍스트, 그리고 심지어 암호화 방법도 주어졌으니
암호화를 두번하고, 나오는 cipher text를 주어진 암호문과 비교 하는방법을 생각했다.

당연히, NP-hard Problem 이라고 생각했다. 그 외의 방법이 있나.. 싶었는데 내가 멍청이란걸 깨닫기전엔...
그래도 모르니 일단 코드 공유를 한다.
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os,sys,timeit,string,base64
from Crypto.Cipher import AES,DES

__author__ = "Song Myeong-Uk"
__date__ = "2015.05.27"

#global value
md5_keys = {}
target_plaintext = ""
encrypted_plaintext = ""

start_time = timeit.default_timer() #when program start

#@func: download_data
#@desc: If there is no md5 hash passwords, download it
def download_data():
  print "[-] passwords.txt not found! downloading now..."
  url = "http://www.ece.ubc.ca/~hyoung/passwords.txt"
  urllib.urlretrieve(url, 'passwords.txt')
  print "[-] passwords.txt download complete"

#@func: save_input_data
#@desc: Insert input data to global variable
def save_input_data():
  global target_plaintext
  global encrypted_plaintext

  i = 0
  with open('PlaintextCiphertext.txt', 'r') as f:
    i = 0
    for line in f:
      line = line.replace("\r\n","").replace("\n","")
      if i is 0:
        target_plaintext = line
      elif i is 1:
        encrypted_plaintext = line
      i = i + 1
  print "[-] Done!"

#@func: download_data
#@desc: To faster calculate. insert file data in memory instead IO system
def md5_to_memory():
  global md5_keys
  with open('passwords.txt', 'r') as f:
    for line in f:
      key_hash = line.split(" ")[0]
      password = line.split(" ")[1].replace("\r\n","").replace("\n","")
      md5_keys[key_hash] = password
  print "[-] Done!"

#@func: DES_encrypt
#@param hash: first selected key of text file
#@desc: Simple DES(ECB) encryptor. return encrypted text
def DES_encrypt(hash):
  global target_plaintext

  des = DES.new(hash[:8], DES.MODE_ECB)
  text = pad(target_plaintext,8)
  cipher_text = des.encrypt(text)

  return cipher_text

#@func: AES_encrypt
#@param DESed: DES encrypted text
#@param hash: Second selected key of text file
#@desc: Simple AES(CBC) Encryptor. return encoded base64 text 
def AES_encrypt(DESed,hash):
  global target_plaintext
  
  #128 bits = 16bytes

  mode = AES.MODE_CBC
  encryptor = AES.new(hash, mode, IV='\x00'*16)
  text = pad(DESed,16)

  ciphertext = encryptor.encrypt(text)
  return ciphertext.encode('base64')

def s2b(x):
  return ''.join(c.encode('hex') for c in x)

#@func: decrypt
#@param enc: encrpyted text
#@param k1: First selected key of text file
#@param k2: First selected key of text file
#@desc: just test case
def decrypt(enc,k1,k2):
  # enc = "W7n515icc+1dW5+82CYgPOGyFqgaFLA0FTCgB/jw1DZBeKUUF2h4z7yRWb03ZIeE"
  # k1 = "0001245350b5eb0a1548fc6d27d3b4d1"
  # k2 = "00009965"
  text = enc
  dec = AES.new(k1, AES.MODE_CBC, IV='\x00'*16)
  des = DES.new(k2[:8], DES.MODE_ECB)
  return unpad(des.decrypt(pad(unpad(dec.decrypt(text.decode('base64'))),8)))

#@func: pad
#@param x: text
#@param b: limit
#@desc: add null character to text untill b
def pad(x,b):
  pad1 = lambda s: s + (b - len(s) % b) * chr(b - len(s) % b) 
  return pad1(x)

def unpad(x):
  try:
    unpad1 = lambda s : s[0:-ord(s[-1])]
    return unpad1(x)
  except Exception,err:
    return x
  

#@func: progress
#@desc: execute calc when it ready
def progress():
  global md5_keys
  try:
    for hash in md5_keys:
     for hash2 in md5_keys:
       DESed_text = DES_encrypt(hash)
       if encrypted_plaintext is AES_encrypt(DESed_text,hash2):
         print "[-] find keys ... !!"
         print md5_keys[hash]
         print md5_keys[hash2]
         raise BreakAllTheLoops()
       else:
        #debug to print. you can pass it
        #pass
        print "[-] Trying key : {} - {},{}".format(AES_encrypt(DESed_text,hash2),md5_keys[hash],md5_keys[hash2]).replace("\r\n","").replace("\n","")
    print "[-] Can not find keys ... "
  except BreakAllTheLoops:
    pass

  # for hash in md5_keys:
  #   for hash2 in md5_keys:
  #     if target_plaintext is decrypt(encrypted_plaintext,hash2,hash):
  #        print hash
  #        print hash2

# main execution
if __name__ == "__main__":
  print "[+] starting {}...".format(os.path.basename(__file__))
  # check if data file exists

  #check input file exists
  cipher_exists = os.path.isfile('PlaintextCiphertext.txt')
  print "[+] checking input file: PlaintextCiphertext.txt"
  
  if not cipher_exists:
    print "[+] PlaintextCiphertext.txt not found!"
    sys.exit()
  else:
    print "[-] Done!"

  print "[+] checking password data: passwords.txt"
  txt_exists = os.path.isfile('passwords.txt')
  if not txt_exists:
    download_data()
  else:
    print "[-] Done!"

  print "[+] Process init ... [1/2]"
  save_input_data()

  print "[+] Process init ... [2/2]"
  md5_to_memory()

  print "[+] Start processing ... "
  print "[-] Target plaintext : {}".format(target_plaintext)
  print "[-] Target Encrpyted key : {}".format(encrypted_plaintext)
  progress()

  stop_time = timeit.default_timer() #when program end

  print "Done!!"
  print "Program Run Time : " + str(stop_time - start_time)


코드는 단순하다. 먼저 DES를 하고 AES를 해서 최종
총 키가 18만개니까, 최악의 경우를 고려하면 O(n^2) 가 되는 방법이다.
1초에 30개씩 처리한다고 해도 며칠은 돌려야된다.. 잘될리가 없었다. 속도문제도 그렇고 퍼포먼스가 나오지 않았다.
이대로는 안되겠다 생각했다. 문제를 다시 읽어서 교수님이 원하는게 뭔가 생각을 했다.

왜 하필이면 두번만했을까, 왜 더블 AES도 아니고 다른 암호화를 썼을까
처음에 접근했어야 하는 문제를 개삽질 한번하고 다시 생각해보게 되었다.

그 과정에 Double DES를 생각하게 되었다.
Double DES 에 대한건 영상을 하나 첨부한다.

요점만 말하자면, 첫번째 암호화 한 결과와 암호화된 값을 복호화함으로서 생기는 중간값이 매치가 되야 정상 프로세스가 진행되게 된다. 즉, 중간값을 알아내기 위한 MITM(Meet in the middle) 공격이 가능해진다. (기존의 MITM이랑은 좀 다르다)

따라서, AES는 decrpyt 하고 DES는 encrpyt 하면 반드시 매칭되는 중간 값이 나오게 된다.
속도도 O(nx2)가 맥시멈으로서 제곱과는 비교가 되지 않는건 물론이고...
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os, sys, time, base64, string, urllib
from Crypto.Cipher import AES, DES
from Crypto.Hash import MD5
 
# author information
__author__ = "Song Myeong-Uk"
__email__ = "mwsong@imtl.skku.ac.kr"
__date__ = "2015.05.27"
 
# constants
DATA_FILE_URL = "http://www.ece.ubc.ca/~hyoung/passwords.txt"
DATA_FILE_NAME = "passwords.txt"
INPUT_FILE_NAME = "PlaintextCiphertext.txt"
OUTPUT_FILE_NAME = "keys.txt"
 
# global variables
md5_keys = {}
target_plaintext = ""
encrypted_plaintext = ""
DES_keys = {}
AES_keys = {}
 
# pad value with given block size: 16
def pad(x):
  BLOCK_SIZE = 16
  if len(x) == BLOCK_SIZE: return x
  c = (len(x) // BLOCK_SIZE) + 1
  c *= BLOCK_SIZE
  return "{:\x00<{l}}".format(x, l=c)
 
# returns value of md5 for given value
def hashMD5 (v):
  m = MD5.new(v)
  return m.digest()
 
# download base md5 hash password list from url
def download_data():
  global DATA_FILE_URL, DATA_FILE_NAME
  print "[-] {} not found! downloading now...".format(DATA_FILE_NAME)
  urllib.urlretrieve(DATA_FILE_URL, DATA_FILE_NAME)
  print "[-] {} download complete".format(DATA_FILE_NAME)
 
# calculate run time
def print_time(t):
  diff = time.time() - t
  print "[*] total run time: {:.4f} seconds".format(diff)
 
# load data from input file
def save_input_data():
  global target_plaintext, encrypted_plaintext
  global INPUT_FILE_NAME
  with open(INPUT_FILE_NAME, 'r') as f:
    data = [ l.strip() for l in f.readlines() ]
    target_plaintext = data[0]
    encrypted_plaintext = data[1]
 
# load md5 data into dictionary
def md5_to_memory():
  global md5_keys
  global DATA_FILE_NAME
  with open(DATA_FILE_NAME, 'r') as f:
    data = [ l.strip() for l in f.readlines() ]
    for line in data:
      line = line.split(' ')
      md5_keys[line[0]] = line[1]
 
# encrypts plaintext with DES using different keys
def plaintext_to_des():
  global md5_keys, DES_keys
  for k in md5_keys:
    KEY = DES_encrypt(hashMD5(md5_keys[k])[:8]).encode('hex')[:64]
    DES_keys[KEY] = md5_keys[k]
 
# decrypts ciphertext with AES128 using different keys
def enc_to_decrypt():
   global encrypted_plaintext, md5_keys, AES_keys, DES_keys
   for k in md5_keys:
    KEY = AES_decrypt(encrypted_plaintext, hashMD5(md5_keys[k])).encode('hex')[:64]
    AES_keys[KEY] = md5_keys[k]
    try:
      if DES_keys[KEY]:
        print "[!] solution key found"
        print "[-] key1: {}".format(DES_keys[KEY])
        print "[-] key2: {}".format(AES_keys[KEY])
        write_key(DES_keys[KEY], AES_keys[KEY])
      else:
        print "[!] solution key not found"
    except Exception, err:
      pass
 
# writes found key to file
def write_key(k1, k2):
  global OUTPUT_FILE_NAME
  print "[+] writing key file...",
  with open(OUTPUT_FILE_NAME, 'wb') as f:
    f.write(k1 + '\n' + k2)
  print "done"
 
# DES-ECB encrypt with given key
def DES_encrypt(key):
  global target_plaintext
  return DES.new(key, DES.MODE_ECB).encrypt(pad(target_plaintext))
 
# AES-CBC decrypt text with given key
def AES_decrypt(text, key):
  return AES.new(key, AES.MODE_CBC, IV='\x00'*16).decrypt(base64.b64decode(text))
 
# main execution block
def main():
  print "[+] starting {}...".format(os.path.basename(__file__))
  # time function execution
  start_time = time.time()
  # check if base data file exists
  txt_exists = os.path.isfile(DATA_FILE_NAME)
  print "[+] checking password data: {}".format(DATA_FILE_NAME)
  if not txt_exists:
    download_data()
  # check if input file exists
  cipher_exists = os.path.isfile(INPUT_FILE_NAME)
  print "[+] checking input file: {}".format(INPUT_FILE_NAME)
  if not cipher_exists:
    print "[!] {} not found! terminating...".format(INPUT_FILE_NAME)
    print_time(start_time)
    sys.exit()
  # load input data
  print "[+] loading input file data..."
  save_input_data()
  # load md5 to memory
  print "[+] preparing md5 dictionary..."
  md5_to_memory()
  # DES encrypt
  print "[+] processing with DES... [1/2]"
  plaintext_to_des()
  # AES decrypt
  print "[+] processing with AES... [2/2]"
  enc_to_decrypt()
  # time execution
  print_time(start_time)
 
if __name__ == "__main__":
  main()


7초만에 프로세스는 끝난게 된다.
여러 다른 어프로치를 했봤으나, 실질적인 소득은 위 두가지 코드가 전부이고, 따로 레퍼런스랄것도 이번에 없다.
Meet-in-the-Middle 공격을 잘 이해만 한다면 쉽게 구현할 수 있다.

최근에는 Single DES 및 Double DES는 아예 쓰이지도 않고, 중간자 공격에 대비해 최소 Triple DES를 사용하고,
이마저도 AES가 표준이 됨으로서 DES가 많이 사용되고 있지는 않지만, 암호화를 하려는 사람들은 이런 Known Ciphertext Attack 을 항상 염두에 둬야할것이다.






Reference
https://www.youtube.com/watch?v=vROZGQ9XLe8
2015/05/28 23:42 2015/05/28 23:42
Assignment #1

Requirements
1. At least 8 characters long
2. Contains both upper and lower-case letters
3. Contains one or more numerical digits and special characters.
4. Must be written in C/C++/Java/Python
5. Time limit for key/100 seconds
6. No salt keys. -> http://www.xorbin.com/tools/sha256-hash-calculator
7. All Program is smaller than 30MB



블로그 글을 너무 대충 작성해서 다시 쓰기로 했다. 구글에 검색하니 나오는게 신기하기도 했고..

먼저 이 과제의 목적은, 일반적으로 알려진 비밀번호를 사용하면 안된다는 것이다.
대부분의 비밀번호로 사용되는 hash 값들은 salt를 이용하여 이중 비밀번호를 진행한다.
따라서 one way 암호화를 진행하는데, 이 과정을 거치지 않을 경우에 비밀번호가 저장된 데이터베이스가 털렸을시 비밀번호 guessing이 가능하다는것을 알려주고자 하는 과정이다

요즘은 비밀번호 자릿수를 무조건 8자리이상에, 특문 하나를 넣어야 된다지만,
예전엔 그렇지 않았다는것을 누구나 안다. 6자리이상에 숫자 안넣어도, 아니, 숫자만 넣어도 비밀번호를 넣을 수 있었다.
이런 취약점들덕에 비밀번호 제도가 바뀌게 되었는데... 사설이 너무 길었고

어쨌든 이 과제에서는 SHA256으로 암호화된 비밀번호를 역으로 알아내야 하는것인데, 공격법은 세가지 정도로 나뉜다.

1. 역상 공격(preimage Attack)
  Secure한 SHA256상대로 할 수 없다. 할 수 있을진 모르겠는데 일단 내 실력으론 어림도 없는건 확실했다.

2. 무차별 대입 공격(Brute force Attack)
  SHA256에 할당된 8자리의 레인보우 테이블만 4테라바이트라고 한다....

3. 사전 공격(Dictionary Attack)
  결국 세가지 방법중에 미리 정의한 잘 알려진 단어와 유출된 사용자들이 자주 쓰던 비밀번호를 사용해서 사전을 만들고, 그를 SHA256 hash로 다시 정의하여 찾는 방법이 합당해보였다


우선 Password Dictionary라고 검색하면 어렵지 않게 정의된 사전들을 찾을 수 있다.
처음에 접근해보길, 모든 단어를 커버할 수 없으니 8자리만을 선택하자. 그런데 대소문자가 반드시 필요하고 특문 하나 숫자 하나도 들어가니까, 임의의 글자를 만들어 넣자라고 생각했으나, 프로그램 크기가 30메가 제한이라는 소식을 접하게 되었다.

처음엔 6자리 단어에 임의로 특문도 넣고 그랬는데 그랬더니 사전의 크기가 너무 커지게 되어서 비효율적이게 되었다.
그리고 만들어놓고 보니 Pass1[#2 뭐 이런꼴인데 사실 이렇게 비번 쓰는 사람도 없을 것 같았다..
그래서 딕셔너리를 다음과 같은 조건으로 거르고 해싱을 해보았다.

조건은,
1. 8~20자리의 글자
2. 숫자가 포함되어 있을것
3. 대문자가 포함되어 있을 것
i = 1
with open("file.txt") as f:
 for line in f:
     raw_pw = line.split(":")[0].replace('\r\n','').replace('\n','')
     if len(raw_pw) < 20 and len(raw_pw) >= 8:
          if re.search("[\!\-\~]",raw_pw) > -1:
               if re.search("[0-9]",raw_pw) > -1: 
                  if re.search("[A-Z]",raw_pw) > -1: 
                     hash_text = hashlib.sha256(raw_pw).hexdigest()
                     fp = open("aa.out","a")
                     fp.write(str(i)+":"+raw_pw+":"+hash_text+"\r\n")
                     fp.close()
                     i += 1

더 깔끔하게 코딩할 수 있을것 같은데 일단 이런식으로 해봤다..
이렇게 하면 백몇십메가나 되던 사전의 크기가 엄청나게 압축된다.
실제로 백만줄이 넘어가던 사전이 1메가도 안된다 지금은..
크기는 30메가 제한인데 제출과 현재 블로그 글을 쓰면서도 불안하나 그냥 하늘에 맡기기로 했다..
그 결과는 이곳(Github)에 있다.

나는 사실 여기까지 만들면서 사전만 잘 구성하면 될거라고 생각 했다. 사실 그게 핵심이고..
하지만 문제가 있었는데, 제출당일날 속도 제한이 있다는것을 깨달았다.
테스트로 돌려본 코드는 I/O를 이용해서 1개 키를 매칭해서 찾고 매칭해서 찾고 하는것이었다.
이건 망했으니 github엔 올리지 않고 여기에서 다룬다..
 import timeit
start = timeit.default_timer()

i = 1
with open("hashedPasswords.txt") as target:
      for line in target:
          user = line.split(":")[1].replace("\r\n","")
          target_word = line.split(":")[2].replace("\r\n","")
          with open("out_a.out") as dic:
              for dic_word in dic:
                  if target_word == dic_word.split("|")[1].replace("\r\n",""):
                      findit = dic_word.replace("\r\n","")
                      fp = open("Passwords.txt","a")
                      fp.write(str(i)+":"+user+":"+findit.split("|")[0]+"\r\n")
                      fp.close()
                      i+=1
                      break
                  
stop = timeit.default_timer()
print "Program Run Time : " + str(stop - start)


굳이 이름을 붙여보자면, Find word in large text file using python... zzz...
기가 막히지만, 말그대로다. 파일 포인터를 두개 열고 일대일로 나올때까지 매칭된다.
아마 사전의 크기가 크고 시도되는 키가 많다면 밤새도록 돌려야될지도 모른다.
내가 쓰려고 짰던건 아니고... 뭐 그렇다.

나는 이거랑 다르게 쓰레드를 이용해보기로 했다.
Python은 싱글코어만 사용하기때문에 쓰레드가 좋지 않아 프로세스를 늘리는 방식으로 사용해야 된다고 본적이 있다.
(참고: 파이썬으로 클라우드 하고 싶어요)

그래서 애초에 프로세스를 한개 더 늘려서 만들어보기로 했다.
사실 syncronized variable을 어떻게 만들어야될지 감도 안와서 추잡하게 다 쓰고 다시 읽어서 넘버링하고, 임시파일을 지우는 꽁수를 적어놨는데, 나는 코드의 신에게 분명히 맞아죽을것이다...
어쨌든 시도 자체는 훌륭했다고 생각한다. 프로세스를 물려서 뭔가 처릴 해보려고 했으니....... 남는건 있었겠지.
이렇게 돌린 코드는... 600초가 소모됐다 -_-
#!/usr/bin/env python

# Author     : Myeong-Uk (mwsong@imtl.skku.ac.kr)
# Date       : 2015. 03. 30
# Desc       : Python program to dictionary attack SHA256
# Dependency : out_c.out (Password dictionary), hashedPasswords.txt(input)
# Command    : <python find.py>
# OUTPUT     : Passwords.txt

import timeit,os
from multiprocessing import Process,Lock
  
  
start_time = timeit.default_timer() #when program start

def do_work(type):
    hash_count = 0
    with open('aa.out') as target:
         for line in target:
            findit = ""
            if type == "1" and hash_count%2 == 0:
                findit = search_word(line)
            elif type== "2" and hash_count%2 > 0:
                findit = search_word(line)
            hash_count +=1

            if findit != "" and findit != None:
                 user = findit.split("|")[0]
                 crackedPassword = findit.split("|")[1]

                 fp = open("Passwords_tmp.txt","a")
                 fp.write(user+":"+crackedPassword+"\r\n")
                 fp.close()

  
def search_word(word):
    user = str(word.split(":")[1].replace("\r\n",""))
    target_hash = word.split(":")[2].replace("\r\n","")

    with open("out_c.out") as dic:
        for dic_line in dic:

            dictionary_hash = dic_line.split("|")[1].replace("\r\n","")
            dictionary_word = dic_line.split("|")[0].replace("\r\n","")

            if target_hash == dictionary_hash:
                 return user + "|" + dictionary_word
                 break
 
def numbering():
    i = 0
    with open("Passwords_tmp.txt") as dic:
        for find_word in dic:    
             fp = open("Passwords.txt","a")
             fp.write(str(i)+":"+find_word)
             fp.close()
             i += 1
    print str(i) + " Password(s) found."

 
     
if __name__ == '__main__':

    pr1 = Process(target=do_work,args=(str(1)))
    pr2 = Process(target=do_work,args=(str(2)))
      
    pr1.start()
    pr2.start()
      
    pr1.join()
    pr2.join()


    stop_time = timeit.default_timer() #when program end

    print "Dictionary matching done"

    print "Numbering.."
    numbering()
    os.remove("Passwords_tmp.txt")
    print "Done!!"

    print "Program Run Time : " + str(stop_time - start_time)


사실 이걸 다 짜고 낼려는데 그제서야 시간제한이 있다는걸 알게 됐다
이대로 내면 100초에 끊을 경우 넘버링으로 다시 쓰는것도 안되기때문에 실제로 다 찾더라도 0이 될께 뻔했다
고로 이 코드는 못쓰는 코드라고 생각하고 다시 생각을 하기 시작했다.

다시 생각을 해보니까, 이런 생각이 들었다. 얘네를 왜 굳이 I/O를 이용해야 하는가?에 대한..
애초에 작은 사전을 만들었으니까, 모두 string 화 해서 사용하면 안될까? 생각이 들었다.
이때 stack overflow에서 찾은 이 글에서 영감을 많이 받았다.
요는, 파이썬은 루프는 느리다. 램에 다 올려서 string으로 찾아라. 그건 굉장히 빠르다.
그래서 덤프를 뜨고 메모리에서 비교를 해보기로 했다.
import timeit,re
from multiprocessing import Process
  
  
start_time = timeit.default_timer() #when program start

pass_dump = ""
hash_dump = ""


def init():
    global pass_dump, hash_dump
    f = open("out_c.out","r")
    pass_dump = f.read()

    f = open("aa.out")
    hash_dump = f.read()



def do_work():
   lines = hash_dump.split("\r\n")
   for i in lines:
    search_word(i.split(":")[2])
  
def search_word(word):
    match = re.search(r'.*'+word,pass_dump)
    if match:
        print match.group(0)
     
if __name__ == '__main__':

    init()

    do_work()

    stop_time = timeit.default_timer() #when program end

    print "Dictionary matching done"

    print "Numbering.."
    #numbering()
    #os.remove("Passwords_tmp.txt")
    print "Done!!"

    print "Program Run Time : " + str(stop_time - start_time)


요점은 한번에 싹 읽고, 정규식을 이용해 해당 라인을 다 가져와서 처리해보겠다고 한것이다.
정규식쓰면 당연히 빠르겠거니 하고 생각했는데, 어찌됐던 이전보단 빨라보이긴했는데 아니 끝이 안난다.
이 코드 검증은 안해서 잘 모르겠지만 그냥 망했다는 삘이 왔다. 너무 느렸다..

그래서 먼저 한 후배한테 물어보니까 그냥 딕셔너리 기본 옵션으로 구현했다고 한다.
무슨차이가 있을까.. 정규식보다 더 빠를 수 있을까? 속는 심정으로 코드를 짰다.
import timeit

start_time = timeit.default_timer() #when program start

pass_dump = {}
hash_dump = {}
count = 0

def init():
    global pass_dump, hash_dump
    with open("dictionary.out","r") as dic:
       for a in dic:
        key = a.split("|")[1].replace("\r\n","").replace("\n","") #hash
        value = a.split("|")[0] #original text
        pass_dump[key] = value

    with open("hashedPasswords.out","r") as dic:
       for a in dic:
        key = str(a.split(":")[2].replace("\r\n","").replace("\n","")) #hash
        value = a.split(":")[1] #user
        hash_dump[key] = value

    print "Init finished.."

def do_work():
    global hash_dump,pass_dump,count

    for a in hash_dump:
        try:
            if pass_dump[a]:
                count += 1
                fp = open("Passwords.txt","a")
                fp.write(str(count)+":"+hash_dump[a]+":"+pass_dump[a]+"\r\n")
                fp.close()
        except KeyError:
            pass
  
def search_word(word):
    match = re.search(r'.*'+word,pass_dump)
    if match:
        print match.group(0)
     
if __name__ == '__main__':

    init()
    do_work()

    stop_time = timeit.default_timer() #when program end

    print "Done!!"
    print str(count) + " Password(s) found. Recorded at Passwords.txt"
    print "Program Run Time : " + str(stop_time - start_time)


겉치레 이런거 없이, 이 코드는 그냥 엔터치면 검색이 끝났다...
배열처럼 사용하는 딕셔너리의 키값에 해쉬를 넣고 반환되는 값이 있나 없나 비교해보는것으로 모든 면에서 우수한것을 확인했다.
원리는 잘 모르겠다. 도대체 나는 동기화 이딴건 왜 찾아보고 있었나 이런 의문이 그냥 들었을뿐....
그냥 파이썬이 딕셔너리 키를 매칭해 찾는 속도가 개빠르구나.. 생각만 들었다. 혹시 구성할일 있으면 딕셔너리가 짱이니 잘쓰도록..

어쨌든, 쉽게 접근하면 쉽게 끝낼 수 있었던 과제였는데 너무 꼬아서 생각하는 바람에,
시간은 시간대로 잡아먹고 결과는... 안나왔지만 뻔할거라 생각한다만..

그래도 간만에 재밌는 과제였다 생각한다. 이런 짱구를 굴리는 과제가 재밌고 좋다.
이 개삽질이 누군가에게 도움되길 바라며 github 링크를 조심스럽게 링킹한다..



Reference
https://sites.google.com/site/reusablesec/Home/password-cracking-tools/probablistic_cracker
http://ko.wikipedia.org/wiki/%EC%97%AD%EC%83%81_%EA%B3%B5%EA%B2%A9
http://www.laurentluce.com/posts/python-and-cryptography-with-pycrypto/
http://www.xorbin.com/tools/sha256-hash-calculator
https://wiki.skullsecurity.org/Passwords
http://www.thegeekstuff.com/2014/07/advanced-python-regex/
http://stackoverflow.com/questions/8023306/get-key-by-value-in-dictionary
http://stackoverflow.com/questions/3449384/fastest-text-search-method-in-a-large-text-file

2015/03/26 11:50 2015/03/26 11:50